diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..d1f790f --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,61 @@ +name: Tests + +on: + push: + branches: + - main + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: '3.9' + - uses: psf/black@stable + with: + options: "--check --verbose" + + run-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: '3.9' + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install poetry + - name: Install project and its dependencies + run: poetry install + - name: Run tests + run: poetry run pytest --cache-clear tests + + comment-coverage: + if: github.event.pull_request + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: recursive + - name: Setup Python + uses: actions/setup-python@v1 + with: + python-version: '3.9' + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install poetry + - name: Install project and its dependencies + run: poetry install + - name: Build coverage file + run: poetry run pytest --cache-clear --cov=urtypes tests > pytest-coverage.txt + - name: Comment coverage + uses: coroo/pytest-coverage-commentator@v1.0.2 diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..31ad4af --- /dev/null +++ b/poetry.lock @@ -0,0 +1,375 @@ +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "black" +version = "22.1.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = ">=1.1.0" +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.0.4" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.3.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.1" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyparsing" +version = "3.0.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "6.2.5" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-mock" +version = "3.7.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "tox", "pytest-asyncio"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-extensions" +version = "4.1.1" +description = "Backported and Experimental Type Hints for Python 3.6+" +category = "dev" +optional = false +python-versions = ">=3.6" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9.1" +content-hash = "6cc1c68509b5c33003c99ef6642c26ddceecb68f48865fdc459510ffe47cd69e" + +[metadata.files] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +black = [ + {file = "black-22.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1297c63b9e1b96a3d0da2d85d11cd9bf8664251fd69ddac068b98dc4f34f73b6"}, + {file = "black-22.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2ff96450d3ad9ea499fc4c60e425a1439c2120cbbc1ab959ff20f7c76ec7e866"}, + {file = "black-22.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e21e1f1efa65a50e3960edd068b6ae6d64ad6235bd8bfea116a03b21836af71"}, + {file = "black-22.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f69158a7d120fd641d1fa9a921d898e20d52e44a74a6fbbcc570a62a6bc8ab"}, + {file = "black-22.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:228b5ae2c8e3d6227e4bde5920d2fc66cc3400fde7bcc74f480cb07ef0b570d5"}, + {file = "black-22.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b1a5ed73ab4c482208d20434f700d514f66ffe2840f63a6252ecc43a9bc77e8a"}, + {file = "black-22.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35944b7100af4a985abfcaa860b06af15590deb1f392f06c8683b4381e8eeaf0"}, + {file = "black-22.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7835fee5238fc0a0baf6c9268fb816b5f5cd9b8793423a75e8cd663c48d073ba"}, + {file = "black-22.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dae63f2dbf82882fa3b2a3c49c32bffe144970a573cd68d247af6560fc493ae1"}, + {file = "black-22.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa1db02410b1924b6749c245ab38d30621564e658297484952f3d8a39fce7e8"}, + {file = "black-22.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8226f50b8c34a14608b848dc23a46e5d08397d009446353dad45e04af0c8e28"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2d6f331c02f0f40aa51a22e479c8209d37fcd520c77721c034517d44eecf5912"}, + {file = "black-22.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:742ce9af3086e5bd07e58c8feb09dbb2b047b7f566eb5f5bc63fd455814979f3"}, + {file = "black-22.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fdb8754b453fb15fad3f72cd9cad3e16776f0964d67cf30ebcbf10327a3777a3"}, + {file = "black-22.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5660feab44c2e3cb24b2419b998846cbb01c23c7fe645fee45087efa3da2d61"}, + {file = "black-22.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:6f2f01381f91c1efb1451998bd65a129b3ed6f64f79663a55fe0e9b74a5f81fd"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:efbadd9b52c060a8fc3b9658744091cb33c31f830b3f074422ed27bad2b18e8f"}, + {file = "black-22.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8871fcb4b447206904932b54b567923e5be802b9b19b744fdff092bd2f3118d0"}, + {file = "black-22.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccad888050f5393f0d6029deea2a33e5ae371fd182a697313bdbd835d3edaf9c"}, + {file = "black-22.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07e5c049442d7ca1a2fc273c79d1aecbbf1bc858f62e8184abe1ad175c4f7cc2"}, + {file = "black-22.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:373922fc66676133ddc3e754e4509196a8c392fec3f5ca4486673e685a421321"}, + {file = "black-22.1.0-py3-none-any.whl", hash = "sha256:3524739d76b6b3ed1132422bf9d82123cd1705086723bc3e235ca39fd21c667d"}, + {file = "black-22.1.0.tar.gz", hash = "sha256:a7c0192d35635f6fc1174be575cb7915e92e5dd629ee79fdaf0dcfa41a80afb5"}, +] +click = [ + {file = "click-8.0.4-py3-none-any.whl", hash = "sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1"}, + {file = "click-8.0.4.tar.gz", hash = "sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"}, + {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"}, + {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"}, + {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"}, + {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"}, + {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"}, + {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"}, + {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"}, + {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"}, + {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"}, + {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"}, + {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"}, + {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"}, + {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"}, + {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"}, + {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"}, + {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"}, + {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"}, + {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"}, + {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"}, + {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"}, + {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"}, + {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"}, + {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyparsing = [ + {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, + {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, +] +pytest = [ + {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, + {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +pytest-mock = [ + {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"}, + {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typing-extensions = [ + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6ad19b0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,36 @@ +# The MIT License (MIT) + +# Copyright (c) 2021 Tom J. Sun + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +[tool.poetry] +name = "urtypes" +version = "0.1.0" +description = "Python implementation of the Blockchain Commons UR Types specification" +authors = ["Jeff S "] + +[tool.poetry.dependencies] +python = "^3.9.1" + +[tool.poetry.dev-dependencies] +pytest = "^6.2.5" +pytest-mock = "^3.6.1" +pytest-cov = "^3.0.0" +black = "^22.1.0" \ No newline at end of file diff --git a/src/urtypes/__init__.py b/src/urtypes/__init__.py index c47e400..b053dd0 100644 --- a/src/urtypes/__init__.py +++ b/src/urtypes/__init__.py @@ -21,4 +21,4 @@ # THE SOFTWARE. from .registry import * -from .bytes import * \ No newline at end of file +from .bytes import * diff --git a/src/urtypes/bytes.py b/src/urtypes/bytes.py index ebec149..797cb2e 100644 --- a/src/urtypes/bytes.py +++ b/src/urtypes/bytes.py @@ -22,23 +22,24 @@ from urtypes import RegistryType, RegistryItem -BYTES = RegistryType('bytes', None) +BYTES = RegistryType("bytes", None) + class Bytes(RegistryItem): - def __init__(self, data): - super().__init__() - self.data = data - - def __eq__(self, o): - return self.data == o.data - - @classmethod - def registry_type(cls): - return BYTES - - def to_data_item(self): - return self.data - - @classmethod - def from_data_item(cls, item): - return cls(cls.mapping(item)) \ No newline at end of file + def __init__(self, data): + super().__init__() + self.data = data + + def __eq__(self, o): + return self.data == o.data + + @classmethod + def registry_type(cls): + return BYTES + + def to_data_item(self): + return self.data + + @classmethod + def from_data_item(cls, item): + return cls(cls.mapping(item)) diff --git a/src/urtypes/cbor/__init__.py b/src/urtypes/cbor/__init__.py index 017fa0c..942e02f 100644 --- a/src/urtypes/cbor/__init__.py +++ b/src/urtypes/cbor/__init__.py @@ -24,4 +24,4 @@ from .data import * from .decoder import * -from .encoder import * \ No newline at end of file +from .encoder import * diff --git a/src/urtypes/cbor/data.py b/src/urtypes/cbor/data.py index bfb2270..7958fa8 100644 --- a/src/urtypes/cbor/data.py +++ b/src/urtypes/cbor/data.py @@ -23,41 +23,54 @@ # THE SOFTWARE. # coding: utf-8 + class Tagging(object): - __slots__ = ("tag", "obj") - def __init__(self, tag, obj): - self.tag = tag - self.obj = obj - def __eq__(self, other): - return isinstance(other, Tagging) and self.tag == other.tag and self.obj == other.obj + __slots__ = ("tag", "obj") + + def __init__(self, tag, obj): + self.tag = tag + self.obj = obj + + def __eq__(self, other): + return ( + isinstance(other, Tagging) + and self.tag == other.tag + and self.obj == other.obj + ) + class Mapping(object): - __slots__ = ('map') - def __init__(self, map): - self.map = map - def mapping(obj): - return Mapping(obj) + __slots__ = "map" + + def __init__(self, map): + self.map = map + + def mapping(obj): + return Mapping(obj) + class DataItem(Tagging): - def __init__(self, tag, map): - super().__init__(tag, Mapping(map)) - self.tag = tag - self.map = map - + def __init__(self, tag, map): + super().__init__(tag, Mapping(map)) + self.tag = tag + self.map = map + + class _Undefined(object): - _instance = None + _instance = None + + def __new__(cls, *args, **kwargs): + if not isinstance(cls._instance, cls): + cls._instance = object.__new__(cls, *args, **kwargs) + return cls._instance - def __new__(cls, *args, **kwargs): - if not isinstance(cls._instance, cls): - cls._instance = object.__new__(cls, *args, **kwargs) - return cls._instance + def __str__(self): + return "Undefined" - def __str__(self): - return "Undefined" + def __repr__(self): + return "Undefined" - def __repr__(self): - return "Undefined" Undefined = _Undefined() -__all__ = ["Tagging", "Mapping", "DataItem"] \ No newline at end of file +__all__ = ["Tagging", "Mapping", "DataItem"] diff --git a/src/urtypes/cbor/decoder.py b/src/urtypes/cbor/decoder.py index e0bd1d1..1b0f0bf 100644 --- a/src/urtypes/cbor/decoder.py +++ b/src/urtypes/cbor/decoder.py @@ -28,170 +28,177 @@ from .data import DataItem, Undefined + class InvalidCborError(Exception): - pass + pass + class _Break(InvalidCborError): - def __init__(self): - InvalidCborError.__init__(self, "Invalid BREAK code occurred") + def __init__(self): + InvalidCborError.__init__(self, "Invalid BREAK code occurred") + class Decoder(object): - def __init__(self, input): - self._jump_table = [ - lambda *args: self.decode_integer(*args, sign=False), - lambda *args: self.decode_integer(*args, sign=True), - self.decode_bytestring, - self.decode_textstring, - self.decode_list, - self.decode_dict, - self.decode_tagging, - self.decode_other - ] - self.input = input - - def decode(self): - mtype, ainfo = self._decode_ibyte() - try: - decoder = self._jump_table[mtype] - except KeyError: - raise InvalidCborError("Invalid major type {}".format(mtype)) - return decoder(mtype, ainfo) - - def decode_integer(self, mtype, ainfo, sign=False): - res = self._decode_length(ainfo) - if sign is True: - return -1 - res - else: - return res - - def decode_bytestring(self, mtype, ainfo): - length = self._decode_length(ainfo) - if length is None: - res = bytearray() - while True: - mtype_, ainfo_ = self._decode_ibyte() - if (mtype_, ainfo_) == (7, 31): - break - if mtype_ != 2: - pass - res.extend(self.decode_bytestring(mtype_, ainfo_)) - return bytes(res) - else: - return self._read(length) - - def decode_textstring(self, mtype, ainfo): - length = self._decode_length(ainfo) - if length is None: - res = bytearray() - while True: - mtype_, ainfo_ = self._decode_ibyte() - if (mtype_, ainfo_) == (7, 31): - break - if mtype_ != 3: - pass - res.extend(self.decode_bytestring(mtype_, ainfo_)) - return res.decode("utf-8") - else: - return self._read(length).decode("utf-8") - - def decode_list(self, mtype, ainfo): - length = self._decode_length(ainfo) - if length is None: - res = [] - while True: - try: - res.append(self.decode()) - except _Break: - break - return res - else: - res = [None for _ in range(length)] - for n in range(length): - res[n] = self.decode() - return res - - def decode_dict(self, mtype, ainfo): - length = self._decode_length(ainfo) - if length is None: - res = {} - try: - while True: - key = self.decode() - value = self.decode() - res[key] = value - except _Break: - pass - return res - else: - res = {} - for n in range(length): - key, value = self.decode(), self.decode() - res[key] = value - return res - - def decode_tagging(self, mtype, ainfo): - length = self._decode_length(ainfo) - return DataItem(length, self.decode()) - - def decode_half_float(self, mtype, ainfo): - half = struct.unpack(">H", self._read(2))[0] - valu = (half & 0x7fff) << 13 | (half & 0x8000) << 16 - if ((half & 0x7c00) != 0x7c00): - return math.ldexp(struct.unpack("!f", struct.pack("!I", valu))[0], 112) - return struct.unpack("!f", struct.pack("!I", valu | 0x7f800000))[0] - - def decode_single_float(self, mtype, ainfo): - return struct.unpack(">f", self._read(4))[0] - - def decode_double_float(self, mtype, ainfo): - return struct.unpack(">d", self._read(8))[0] - - def decode_other(self, mtype, ainfo): - if ainfo == 20: - return False - elif ainfo == 21: - return True - elif ainfo == 22: - return None - elif ainfo == 23: - return Undefined - elif ainfo == 25: - return self.decode_half_float(mtype, ainfo) - elif ainfo == 26: - return self.decode_single_float(mtype, ainfo) - elif ainfo == 27: - return self.decode_double_float(mtype, ainfo) - elif ainfo == 31: - raise _Break() - - def _decode_ibyte(self): - byte = self._read(1)[0] - if isinstance(byte, str): - byte = ord(byte) - return (byte & 0b11100000) >> 5, byte & 0b00011111 - - def _decode_length(self, ainfo): - if ainfo < 24: - return ainfo - elif ainfo == 24: - return from_bytes(self._read(1)) - elif ainfo == 25: - return from_bytes(self._read(2)) - elif ainfo == 26: - return from_bytes(self._read(4)) - elif ainfo == 27: - return from_bytes(self._read(8)) - elif ainfo == 31: - return None - raise InvalidCborError("Invalid additional information {}".format(ainfo)) - - def _read(self, n): - m = self.input.read(n) - if len(m) != n: - raise InvalidCborError("Expected {} bytes, got {} bytes instead".format(n, len(m))) - return m + def __init__(self, input): + self._jump_table = [ + lambda *args: self.decode_integer(*args, sign=False), + lambda *args: self.decode_integer(*args, sign=True), + self.decode_bytestring, + self.decode_textstring, + self.decode_list, + self.decode_dict, + self.decode_tagging, + self.decode_other, + ] + self.input = input + + def decode(self): + mtype, ainfo = self._decode_ibyte() + try: + decoder = self._jump_table[mtype] + except KeyError: + raise InvalidCborError("Invalid major type {}".format(mtype)) + return decoder(mtype, ainfo) + + def decode_integer(self, mtype, ainfo, sign=False): + res = self._decode_length(ainfo) + if sign is True: + return -1 - res + else: + return res + + def decode_bytestring(self, mtype, ainfo): + length = self._decode_length(ainfo) + if length is None: + res = bytearray() + while True: + mtype_, ainfo_ = self._decode_ibyte() + if (mtype_, ainfo_) == (7, 31): + break + if mtype_ != 2: + pass + res.extend(self.decode_bytestring(mtype_, ainfo_)) + return bytes(res) + else: + return self._read(length) + + def decode_textstring(self, mtype, ainfo): + length = self._decode_length(ainfo) + if length is None: + res = bytearray() + while True: + mtype_, ainfo_ = self._decode_ibyte() + if (mtype_, ainfo_) == (7, 31): + break + if mtype_ != 3: + pass + res.extend(self.decode_bytestring(mtype_, ainfo_)) + return res.decode("utf-8") + else: + return self._read(length).decode("utf-8") + + def decode_list(self, mtype, ainfo): + length = self._decode_length(ainfo) + if length is None: + res = [] + while True: + try: + res.append(self.decode()) + except _Break: + break + return res + else: + res = [None for _ in range(length)] + for n in range(length): + res[n] = self.decode() + return res + + def decode_dict(self, mtype, ainfo): + length = self._decode_length(ainfo) + if length is None: + res = {} + try: + while True: + key = self.decode() + value = self.decode() + res[key] = value + except _Break: + pass + return res + else: + res = {} + for n in range(length): + key, value = self.decode(), self.decode() + res[key] = value + return res + + def decode_tagging(self, mtype, ainfo): + length = self._decode_length(ainfo) + return DataItem(length, self.decode()) + + def decode_half_float(self, mtype, ainfo): + half = struct.unpack(">H", self._read(2))[0] + valu = (half & 0x7FFF) << 13 | (half & 0x8000) << 16 + if (half & 0x7C00) != 0x7C00: + return math.ldexp(struct.unpack("!f", struct.pack("!I", valu))[0], 112) + return struct.unpack("!f", struct.pack("!I", valu | 0x7F800000))[0] + + def decode_single_float(self, mtype, ainfo): + return struct.unpack(">f", self._read(4))[0] + + def decode_double_float(self, mtype, ainfo): + return struct.unpack(">d", self._read(8))[0] + + def decode_other(self, mtype, ainfo): + if ainfo == 20: + return False + elif ainfo == 21: + return True + elif ainfo == 22: + return None + elif ainfo == 23: + return Undefined + elif ainfo == 25: + return self.decode_half_float(mtype, ainfo) + elif ainfo == 26: + return self.decode_single_float(mtype, ainfo) + elif ainfo == 27: + return self.decode_double_float(mtype, ainfo) + elif ainfo == 31: + raise _Break() + + def _decode_ibyte(self): + byte = self._read(1)[0] + if isinstance(byte, str): + byte = ord(byte) + return (byte & 0b11100000) >> 5, byte & 0b00011111 + + def _decode_length(self, ainfo): + if ainfo < 24: + return ainfo + elif ainfo == 24: + return from_bytes(self._read(1)) + elif ainfo == 25: + return from_bytes(self._read(2)) + elif ainfo == 26: + return from_bytes(self._read(4)) + elif ainfo == 27: + return from_bytes(self._read(8)) + elif ainfo == 31: + return None + raise InvalidCborError("Invalid additional information {}".format(ainfo)) + + def _read(self, n): + m = self.input.read(n) + if len(m) != n: + raise InvalidCborError( + "Expected {} bytes, got {} bytes instead".format(n, len(m)) + ) + return m + __all__ = ["InvalidCborError", "Decoder"] + def from_bytes(val): - return int.from_bytes(val, "big") + return int.from_bytes(val, "big") diff --git a/src/urtypes/cbor/encoder.py b/src/urtypes/cbor/encoder.py index ffb3f17..3e8a03e 100644 --- a/src/urtypes/cbor/encoder.py +++ b/src/urtypes/cbor/encoder.py @@ -25,117 +25,126 @@ import struct -_str_type = type(u"") +_str_type = type("") _bytes_type = type(b"") from .data import Tagging, Mapping, Undefined + class EncoderError(Exception): - pass + pass + class Encoder(object): - def __init__(self, output): - self.output = output - - def encode(self, val): - if isinstance(val, _bytes_type): - self.encode_bytestring(val) - elif isinstance(val, _str_type): - self.encode_textstring(val) - elif isinstance(val, float): - self.encode_float(val) - elif isinstance(val, bool): - self.encode_boolean(val) - elif isinstance(val, int): - self.encode_integer(val) - elif isinstance(val, list): - self.encode_list(val) - elif isinstance(val, dict): - self.encode_dict(val) - elif isinstance(val, Tagging): - self.encode_tagging(val) - elif val is Undefined: - self.encode_undefined() - elif val is None: - self.encode_null() - elif isinstance(val, Mapping): - val = val.map - self.encode(val) - else: - raise EncoderError("val of type {} is not serializable".format(type(val))) - - def encode_list(self, list): - self._write(_encode_ibyte(4, len(list))) - for elem in list: - self.encode(elem) - - def encode_dict(self, dict): - self._write(_encode_ibyte(5, len(dict))) - for key, value in dict.items(): - self.encode(key) - self.encode(value) - - def encode_bytestring(self, bytestring): - self._write(_encode_ibyte(2, len(bytestring))) - self._write(bytestring) - - def encode_textstring(self, textstring): - string_ = textstring.encode("utf-8") - self._write(_encode_ibyte(3, len(string_))) - self._write(string_) - - def encode_float(self, float): - self._write(b"\xfb") - self._write(struct.pack(">d", float)) - - def encode_integer(self, integer): - if integer < 0: - integer = -integer - 1 - try: - self._write(_encode_ibyte(1, integer)) - except TypeError: - raise EncoderError("Encoding integers lower than -18446744073709551616 is not supported") - else: - try: - self._write(_encode_ibyte(0, integer)) - except TypeError: - raise EncoderError("Encoding integers larger than 18446744073709551615 is not supported") - - def encode_tagging(self, tagging): - try: - self._write(_encode_ibyte(6, tagging.tag)) - except TypeError: - raise EncoderError("Encoding tag larger than 18446744073709551615 is not supported") - self.encode(tagging.obj) - - def encode_boolean(self, boolean): - if boolean is True: - self._write(_encode_ibyte(7, 21)) - elif boolean is False: - self._write(_encode_ibyte(7, 20)) - - def encode_null(self): - self._write(_encode_ibyte(7, 22)) - - def encode_undefined(self): - self._write(b"\xf7") - - def _write(self, val): - self.output.write(val) + def __init__(self, output): + self.output = output + + def encode(self, val): + if isinstance(val, _bytes_type): + self.encode_bytestring(val) + elif isinstance(val, _str_type): + self.encode_textstring(val) + elif isinstance(val, float): + self.encode_float(val) + elif isinstance(val, bool): + self.encode_boolean(val) + elif isinstance(val, int): + self.encode_integer(val) + elif isinstance(val, list): + self.encode_list(val) + elif isinstance(val, dict): + self.encode_dict(val) + elif isinstance(val, Tagging): + self.encode_tagging(val) + elif val is Undefined: + self.encode_undefined() + elif val is None: + self.encode_null() + elif isinstance(val, Mapping): + val = val.map + self.encode(val) + else: + raise EncoderError("val of type {} is not serializable".format(type(val))) + + def encode_list(self, list): + self._write(_encode_ibyte(4, len(list))) + for elem in list: + self.encode(elem) + + def encode_dict(self, dict): + self._write(_encode_ibyte(5, len(dict))) + for key, value in dict.items(): + self.encode(key) + self.encode(value) + + def encode_bytestring(self, bytestring): + self._write(_encode_ibyte(2, len(bytestring))) + self._write(bytestring) + + def encode_textstring(self, textstring): + string_ = textstring.encode("utf-8") + self._write(_encode_ibyte(3, len(string_))) + self._write(string_) + + def encode_float(self, float): + self._write(b"\xfb") + self._write(struct.pack(">d", float)) + + def encode_integer(self, integer): + if integer < 0: + integer = -integer - 1 + try: + self._write(_encode_ibyte(1, integer)) + except TypeError: + raise EncoderError( + "Encoding integers lower than -18446744073709551616 is not supported" + ) + else: + try: + self._write(_encode_ibyte(0, integer)) + except TypeError: + raise EncoderError( + "Encoding integers larger than 18446744073709551615 is not supported" + ) + + def encode_tagging(self, tagging): + try: + self._write(_encode_ibyte(6, tagging.tag)) + except TypeError: + raise EncoderError( + "Encoding tag larger than 18446744073709551615 is not supported" + ) + self.encode(tagging.obj) + + def encode_boolean(self, boolean): + if boolean is True: + self._write(_encode_ibyte(7, 21)) + elif boolean is False: + self._write(_encode_ibyte(7, 20)) + + def encode_null(self): + self._write(_encode_ibyte(7, 22)) + + def encode_undefined(self): + self._write(b"\xf7") + + def _write(self, val): + self.output.write(val) + def _encode_ibyte(major, length): - if length < 24: - return struct.pack('>B', (major << 5) | length) - elif length < 256: - return struct.pack('>BB', (major << 5) | 24, length) - elif length < 65536: - return struct.pack('>BH', (major << 5) | 25, length) - elif length < 4294967296: - return struct.pack('>BI', (major << 5) | 26, length) - elif length < 18446744073709551616: - return struct.pack('>BQ', (major << 5) | 27, length) - else: - return None + if length < 24: + return struct.pack(">B", (major << 5) | length) + elif length < 256: + return struct.pack(">BB", (major << 5) | 24, length) + elif length < 65536: + return struct.pack(">BH", (major << 5) | 25, length) + elif length < 4294967296: + return struct.pack(">BI", (major << 5) | 26, length) + elif length < 18446744073709551616: + return struct.pack(">BQ", (major << 5) | 27, length) + else: + return None -__all__ = ["Encoder", "EncoderError"] +__all__ = ["Encoder", "EncoderError"] diff --git a/src/urtypes/crypto/__init__.py b/src/urtypes/crypto/__init__.py index 41df719..e3cca1e 100644 --- a/src/urtypes/crypto/__init__.py +++ b/src/urtypes/crypto/__init__.py @@ -28,4 +28,4 @@ from .keypath import * from .multi_key import * from .output import * -from .psbt import * \ No newline at end of file +from .psbt import * diff --git a/src/urtypes/crypto/account.py b/src/urtypes/crypto/account.py index cfd2760..84e24fa 100644 --- a/src/urtypes/crypto/account.py +++ b/src/urtypes/crypto/account.py @@ -23,32 +23,38 @@ from urtypes import RegistryType, RegistryItem from .output import Output -CRYPTO_ACCOUNT = RegistryType('crypto-account', 311) +CRYPTO_ACCOUNT = RegistryType("crypto-account", 311) + class Account(RegistryItem): - def __init__(self, master_fingerprint, output_descriptors): - super().__init__() - self.master_fingerprint = master_fingerprint - self.output_descriptors = output_descriptors - - def __eq__(self, o): - return self.master_fingerprint == o.master_fingerprint and self.output_descriptors == o.output_descriptors - - @classmethod - def registry_type(cls): - return CRYPTO_ACCOUNT - - def to_data_item(self): - map = {} - if self.master_fingerprint is not None: - map[1] = int.from_bytes(self.master_fingerprint, 'big') - if self.output_descriptors is not None: - map[2] = [descriptor.to_data_item() for descriptor in self.output_descriptors] - return map - - @classmethod - def from_data_item(cls, item): - map = cls.mapping(item) - master_fingerprint = map[1].to_bytes(4, 'big') if 1 in map else None - outputs = [Output.from_data_item(item) for item in map[2]] if 2 in map else None - return cls(master_fingerprint, outputs) \ No newline at end of file + def __init__(self, master_fingerprint, output_descriptors): + super().__init__() + self.master_fingerprint = master_fingerprint + self.output_descriptors = output_descriptors + + def __eq__(self, o): + return ( + self.master_fingerprint == o.master_fingerprint + and self.output_descriptors == o.output_descriptors + ) + + @classmethod + def registry_type(cls): + return CRYPTO_ACCOUNT + + def to_data_item(self): + map = {} + if self.master_fingerprint is not None: + map[1] = int.from_bytes(self.master_fingerprint, "big") + if self.output_descriptors is not None: + map[2] = [ + descriptor.to_data_item() for descriptor in self.output_descriptors + ] + return map + + @classmethod + def from_data_item(cls, item): + map = cls.mapping(item) + master_fingerprint = map[1].to_bytes(4, "big") if 1 in map else None + outputs = [Output.from_data_item(item) for item in map[2]] if 2 in map else None + return cls(master_fingerprint, outputs) diff --git a/src/urtypes/crypto/bip39.py b/src/urtypes/crypto/bip39.py index 83e1238..dcaa535 100644 --- a/src/urtypes/crypto/bip39.py +++ b/src/urtypes/crypto/bip39.py @@ -22,32 +22,31 @@ from urtypes import RegistryType, RegistryItem -CRYPTO_BIP39 = RegistryType('crypto-bip39', 301) +CRYPTO_BIP39 = RegistryType("crypto-bip39", 301) + class BIP39(RegistryItem): - def __init__(self, words, lang): - super().__init__() - self.words = words - self.lang = lang - - def __eq__(self, o): - return self.words == o.words and self.lang == o.lang - - @classmethod - def registry_type(cls): - return CRYPTO_BIP39 - - def to_data_item(self): - map = { - 1: self.words - } - if self.lang is not None: - map[2] = self.lang - return map - - @classmethod - def from_data_item(cls, item): - map = cls.mapping(item) - words = map[1] - lang = map[2] if 2 in map else None - return cls(words, lang) \ No newline at end of file + def __init__(self, words, lang): + super().__init__() + self.words = words + self.lang = lang + + def __eq__(self, o): + return self.words == o.words and self.lang == o.lang + + @classmethod + def registry_type(cls): + return CRYPTO_BIP39 + + def to_data_item(self): + map = {1: self.words} + if self.lang is not None: + map[2] = self.lang + return map + + @classmethod + def from_data_item(cls, item): + map = cls.mapping(item) + words = map[1] + lang = map[2] if 2 in map else None + return cls(words, lang) diff --git a/src/urtypes/crypto/coin_info.py b/src/urtypes/crypto/coin_info.py index 87c50d2..b9354ab 100644 --- a/src/urtypes/crypto/coin_info.py +++ b/src/urtypes/crypto/coin_info.py @@ -22,32 +22,33 @@ from urtypes import RegistryType, RegistryItem -CRYPTO_COIN_INFO = RegistryType('crypto-coin-info', 305) +CRYPTO_COIN_INFO = RegistryType("crypto-coin-info", 305) + class CoinInfo(RegistryItem): - def __init__(self, type, network): - super().__init__() - self.type = type - self.network = network - - def __eq__(self, o): - return self.type == o.type and self.network == o.network - - @classmethod - def registry_type(cls): - return CRYPTO_COIN_INFO - - def to_data_item(self): - map = {} - if self.type is not None: - map[1] = self.type - if self.network is not None: - map[2] = self.network - return map - - @classmethod - def from_data_item(cls, item): - map = cls.mapping(item) - type = map[1] if 1 in map else None - network = map[2] if 2 in map else None - return cls(type, network) \ No newline at end of file + def __init__(self, type, network): + super().__init__() + self.type = type + self.network = network + + def __eq__(self, o): + return self.type == o.type and self.network == o.network + + @classmethod + def registry_type(cls): + return CRYPTO_COIN_INFO + + def to_data_item(self): + map = {} + if self.type is not None: + map[1] = self.type + if self.network is not None: + map[2] = self.network + return map + + @classmethod + def from_data_item(cls, item): + map = cls.mapping(item) + type = map[1] if 1 in map else None + network = map[2] if 2 in map else None + return cls(type, network) diff --git a/src/urtypes/crypto/ec_key.py b/src/urtypes/crypto/ec_key.py index e7ee42d..7441a13 100644 --- a/src/urtypes/crypto/ec_key.py +++ b/src/urtypes/crypto/ec_key.py @@ -23,38 +23,43 @@ import binascii from urtypes import RegistryType, RegistryItem -CRYPTO_ECKEY = RegistryType('crypto-eckey', 306) +CRYPTO_ECKEY = RegistryType("crypto-eckey", 306) + class ECKey(RegistryItem): - def __init__(self, data, curve, private_key): - super().__init__() - self.data = data - self.curve = curve - self.private_key = private_key - - def __eq__(self, o): - return self.data == o.data and self.curve == o.curve and self.private_key == o.private_key - - @classmethod - def registry_type(cls): - return CRYPTO_ECKEY - - def to_data_item(self): - map = {} - if self.curve is not None: - map[1] = self.curve - if self.private_key is not None: - map[2] = self.private_key - map[3] = self.data - return map - - @classmethod - def from_data_item(cls, item): - map = cls.mapping(item) - data = map[3] - curve = map[1] if 1 in map else None - private_key = map[2] if 2 in map else None - return cls(data, curve, private_key) - - def descriptor_key(self): - return binascii.hexlify(self.data).decode() \ No newline at end of file + def __init__(self, data, curve, private_key): + super().__init__() + self.data = data + self.curve = curve + self.private_key = private_key + + def __eq__(self, o): + return ( + self.data == o.data + and self.curve == o.curve + and self.private_key == o.private_key + ) + + @classmethod + def registry_type(cls): + return CRYPTO_ECKEY + + def to_data_item(self): + map = {} + if self.curve is not None: + map[1] = self.curve + if self.private_key is not None: + map[2] = self.private_key + map[3] = self.data + return map + + @classmethod + def from_data_item(cls, item): + map = cls.mapping(item) + data = map[3] + curve = map[1] if 1 in map else None + private_key = map[2] if 2 in map else None + return cls(data, curve, private_key) + + def descriptor_key(self): + return binascii.hexlify(self.data).decode() diff --git a/src/urtypes/crypto/hd_key.py b/src/urtypes/crypto/hd_key.py index 3facf03..fbd4186 100644 --- a/src/urtypes/crypto/hd_key.py +++ b/src/urtypes/crypto/hd_key.py @@ -27,186 +27,233 @@ from .coin_info import CoinInfo from .keypath import Keypath -CRYPTO_HDKEY = RegistryType('crypto-hdkey', 303) +CRYPTO_HDKEY = RegistryType("crypto-hdkey", 303) + class HDKey(RegistryItem): - def __init__(self, props): - super().__init__() - self.master = None - self.key = None - self.chain_code = None - self.private_key = None - self.use_info = None - self.origin = None - self.children = None - self.parent_fingerprint = None - self.name = None - self.note = None - if 'master' in props and props['master']: - self.setup_master_key(props) - else: - self.setup_derive_key(props) - - def __eq__(self, o): - return ( - self.master == o.master and - self.key == o.key and - self.chain_code == o.chain_code and - self.private_key == o.private_key and - self.use_info == o.use_info and - self.origin == o.origin and - self.children == o.children and - self.parent_fingerprint == o.parent_fingerprint and - self.name == o.name and - self.note == o.note - ) - - @classmethod - def registry_type(cls): - return CRYPTO_HDKEY - - def setup_master_key(self, props): - self.master = True - self.key = props['key'] if 'key' in props else None - self.chain_code = props['chain_code'] if 'chain_code' in props else None - - def setup_derive_key(self, props): - self.master = False - self.key = props['key'] if 'key' in props else None - self.chain_code = props['chain_code'] if 'chain_code' in props else None - self.private_key = props['private_key'] if 'private_key' in props else None - self.use_info = props['use_info'] if 'use_info' in props else None - self.origin = props['origin'] if 'origin' in props else None - self.children = props['children'] if 'children' in props else None - self.parent_fingerprint = props['parent_fingerprint'] if 'parent_fingerprint' in props else None - self.name = props['name'] if 'name' in props else None - self.note = props['note'] if 'note' in props else None - - def bip32_key(self, include_derivation_path=False): - parent_fingerprint = (0).to_bytes(4, 'big') - source_is_parent = False - chain_code = self.chain_code if self.chain_code is not None else (0).to_bytes(32, 'big') - key = self.key - if len(key) == 32: - key = 0x00 + key - depth = 0 - index = 0 - if self.master: - version = binascii.unhexlify('0488ADE4' if not self.use_info or self.use_info.network == 0 else '04358394') - else: - if self.private_key: - version = binascii.unhexlify('0488ADE4' if not self.use_info or self.use_info.network == 0 else '04358394') - else: - version = binascii.unhexlify('0488B21E' if not self.use_info or self.use_info.network == 0 else '043587CF') - if self.parent_fingerprint is not None: - parent_fingerprint = self.parent_fingerprint - depth = self.origin.depth if self.origin.depth is not None else len(self.origin.components) - paths = self.origin.components - if len(paths) > 0: - last_path = paths[len(paths) - 1] - index = last_path.index - if last_path.hardened: - index += 0x80000000 - if self.parent_fingerprint is None and self.origin.source_fingerprint is not None and len(paths) == 1: - parent_fingerprint = self.origin.source_fingerprint - source_is_parent = True - depth = depth.to_bytes(1, 'big') - index = index.to_bytes(4, 'big') - key = encode_check(version + depth + parent_fingerprint + index + chain_code + key) - if include_derivation_path: - derivation = '' - if self.origin and self.origin.path() and self.origin.source_fingerprint and not source_is_parent: - derivation = '[%s/%s]' % (binascii.hexlify(self.origin.source_fingerprint).decode('utf-8'), self.origin.path()) - - child_derivation = '' - if self.children and self.children.path(): - child_derivation = '/' + self.children.path() - - return '%s%s%s' % (derivation, key, child_derivation) - return key - - def descriptor_key(self): - return self.bip32_key(True) - - def to_data_item(self): - map = {} - if self.master: - map[1] = True - map[3] = self.key - map[4] = self.chain_code - else: - if self.private_key is not None: - map[2] = self.private_key - map[3] = self.key - if self.chain_code is not None: - map[4] = self.chain_code - if self.use_info is not None: - map[5] = DataItem(self.use_info.registry_type().tag, self.use_info.to_data_item()) - if self.origin is not None: - map[6] = DataItem(self.origin.registry_type().tag, self.origin.to_data_item()) - if self.children is not None: - map[7] = DataItem(self.children.registry_type().tag, self.children.to_data_item()) - if self.parent_fingerprint is not None: - map[8] = int.from_bytes(self.parent_fingerprint, 'big') - if self.name is not None: - map[9] = self.name - if self.note is not None: - map[10] = self.note - return map - - @classmethod - def from_data_item(cls, item): - map = cls.mapping(item) - master = 1 in map and map[1] - private_key = map[2] if 2 in map else None - key = map[3] if 3 in map else None - chain_code = map[4] if 4 in map else None - use_info = CoinInfo.from_data_item(map[5]) if 5 in map else None - origin = Keypath.from_data_item(map[6]) if 6 in map else None - children = Keypath.from_data_item(map[7]) if 7 in map else None - parent_fingerprint = map[8].to_bytes(4, 'big') if 8 in map else None - name = map[9] if 9 in map else None - note = map[10] if 10 in map else None - return cls({ - 'master': master, - 'private_key': private_key, - 'key': key, - 'chain_code': chain_code, - 'use_info': use_info, - 'origin': origin, - 'children': children, - 'parent_fingerprint': parent_fingerprint, - 'name': name, - 'note': note - }) - + def __init__(self, props): + super().__init__() + self.master = None + self.key = None + self.chain_code = None + self.private_key = None + self.use_info = None + self.origin = None + self.children = None + self.parent_fingerprint = None + self.name = None + self.note = None + if "master" in props and props["master"]: + self.setup_master_key(props) + else: + self.setup_derive_key(props) + + def __eq__(self, o): + return ( + self.master == o.master + and self.key == o.key + and self.chain_code == o.chain_code + and self.private_key == o.private_key + and self.use_info == o.use_info + and self.origin == o.origin + and self.children == o.children + and self.parent_fingerprint == o.parent_fingerprint + and self.name == o.name + and self.note == o.note + ) + + @classmethod + def registry_type(cls): + return CRYPTO_HDKEY + + def setup_master_key(self, props): + self.master = True + self.key = props["key"] if "key" in props else None + self.chain_code = props["chain_code"] if "chain_code" in props else None + + def setup_derive_key(self, props): + self.master = False + self.key = props["key"] if "key" in props else None + self.chain_code = props["chain_code"] if "chain_code" in props else None + self.private_key = props["private_key"] if "private_key" in props else None + self.use_info = props["use_info"] if "use_info" in props else None + self.origin = props["origin"] if "origin" in props else None + self.children = props["children"] if "children" in props else None + self.parent_fingerprint = ( + props["parent_fingerprint"] if "parent_fingerprint" in props else None + ) + self.name = props["name"] if "name" in props else None + self.note = props["note"] if "note" in props else None + + def bip32_key(self, include_derivation_path=False): + parent_fingerprint = (0).to_bytes(4, "big") + source_is_parent = False + chain_code = ( + self.chain_code if self.chain_code is not None else (0).to_bytes(32, "big") + ) + key = self.key + if len(key) == 32: + key = 0x00 + key + depth = 0 + index = 0 + if self.master: + version = binascii.unhexlify( + "0488ADE4" + if not self.use_info or self.use_info.network == 0 + else "04358394" + ) + else: + if self.private_key: + version = binascii.unhexlify( + "0488ADE4" + if not self.use_info or self.use_info.network == 0 + else "04358394" + ) + else: + version = binascii.unhexlify( + "0488B21E" + if not self.use_info or self.use_info.network == 0 + else "043587CF" + ) + if self.parent_fingerprint is not None: + parent_fingerprint = self.parent_fingerprint + depth = ( + self.origin.depth + if self.origin.depth is not None + else len(self.origin.components) + ) + paths = self.origin.components + if len(paths) > 0: + last_path = paths[len(paths) - 1] + index = last_path.index + if last_path.hardened: + index += 0x80000000 + if ( + self.parent_fingerprint is None + and self.origin.source_fingerprint is not None + and len(paths) == 1 + ): + parent_fingerprint = self.origin.source_fingerprint + source_is_parent = True + depth = depth.to_bytes(1, "big") + index = index.to_bytes(4, "big") + key = encode_check( + version + depth + parent_fingerprint + index + chain_code + key + ) + if include_derivation_path: + derivation = "" + if ( + self.origin + and self.origin.path() + and self.origin.source_fingerprint + and not source_is_parent + ): + derivation = "[%s/%s]" % ( + binascii.hexlify(self.origin.source_fingerprint).decode("utf-8"), + self.origin.path(), + ) + + child_derivation = "" + if self.children and self.children.path(): + child_derivation = "/" + self.children.path() + + return "%s%s%s" % (derivation, key, child_derivation) + return key + + def descriptor_key(self): + return self.bip32_key(True) + + def to_data_item(self): + map = {} + if self.master: + map[1] = True + map[3] = self.key + map[4] = self.chain_code + else: + if self.private_key is not None: + map[2] = self.private_key + map[3] = self.key + if self.chain_code is not None: + map[4] = self.chain_code + if self.use_info is not None: + map[5] = DataItem( + self.use_info.registry_type().tag, self.use_info.to_data_item() + ) + if self.origin is not None: + map[6] = DataItem( + self.origin.registry_type().tag, self.origin.to_data_item() + ) + if self.children is not None: + map[7] = DataItem( + self.children.registry_type().tag, self.children.to_data_item() + ) + if self.parent_fingerprint is not None: + map[8] = int.from_bytes(self.parent_fingerprint, "big") + if self.name is not None: + map[9] = self.name + if self.note is not None: + map[10] = self.note + return map + + @classmethod + def from_data_item(cls, item): + map = cls.mapping(item) + master = 1 in map and map[1] + private_key = map[2] if 2 in map else None + key = map[3] if 3 in map else None + chain_code = map[4] if 4 in map else None + use_info = CoinInfo.from_data_item(map[5]) if 5 in map else None + origin = Keypath.from_data_item(map[6]) if 6 in map else None + children = Keypath.from_data_item(map[7]) if 7 in map else None + parent_fingerprint = map[8].to_bytes(4, "big") if 8 in map else None + name = map[9] if 9 in map else None + note = map[10] if 10 in map else None + return cls( + { + "master": master, + "private_key": private_key, + "key": key, + "chain_code": chain_code, + "use_info": use_info, + "origin": origin, + "children": children, + "parent_fingerprint": parent_fingerprint, + "name": name, + "note": note, + } + ) + + B58_DIGITS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + def double_sha256(msg): - """sha256(sha256(msg)) -> bytes""" - return hashlib.sha256(hashlib.sha256(msg).digest()).digest() + """sha256(sha256(msg)) -> bytes""" + return hashlib.sha256(hashlib.sha256(msg).digest()).digest() + def encode(b): - """Encode bytes to a base58-encoded string""" - - # Convert big-endian bytes to integer - n = int("0x0" + binascii.hexlify(b).decode("utf8"), 16) - - # Divide that integer into bas58 - res = [] - while n > 0: - n, r = divmod(n, 58) - res.append(B58_DIGITS[r]) - res = "".join(res[::-1]) - - pad = 0 - for c in b: - if c == 0: - pad += 1 - else: - break - return B58_DIGITS[0] * pad + res + """Encode bytes to a base58-encoded string""" + + # Convert big-endian bytes to integer + n = int("0x0" + binascii.hexlify(b).decode("utf8"), 16) + + # Divide that integer into bas58 + res = [] + while n > 0: + n, r = divmod(n, 58) + res.append(B58_DIGITS[r]) + res = "".join(res[::-1]) + + pad = 0 + for c in b: + if c == 0: + pad += 1 + else: + break + return B58_DIGITS[0] * pad + res + def encode_check(b): - """Encode bytes to a base58-encoded string with a checksum""" - return encode(b + double_sha256(b)[0:4]) \ No newline at end of file + """Encode bytes to a base58-encoded string with a checksum""" + return encode(b + double_sha256(b)[0:4]) diff --git a/src/urtypes/crypto/keypath.py b/src/urtypes/crypto/keypath.py index 97560e4..ab2c00c 100644 --- a/src/urtypes/crypto/keypath.py +++ b/src/urtypes/crypto/keypath.py @@ -22,67 +22,83 @@ from urtypes import RegistryType, RegistryItem -CRYPTO_KEYPATH = RegistryType('crypto-keypath', 304) +CRYPTO_KEYPATH = RegistryType("crypto-keypath", 304) + class Keypath(RegistryItem): - def __init__(self, components, source_fingerprint, depth): - super().__init__() - self.components = components - self.source_fingerprint = source_fingerprint - self.depth = depth - - def __eq__(self, o): - return self.components == o.components and self.source_fingerprint == o.source_fingerprint and self.depth == o.depth - - @classmethod - def registry_type(cls): - return CRYPTO_KEYPATH - - def path(self): - if not self.components: - return '' - return '/'.join([('*' if component.wildcard else str(component.index)) + ('\'' if component.hardened else '') for component in self.components]) - - def to_data_item(self): - map = {} - components = [] - for component in self.components: - if component.wildcard: - components.append([]) - else: - components.append(component.index) - components.append(component.hardened) - map[1] = components - if self.source_fingerprint is not None: - map[2] = int.from_bytes(self.source_fingerprint, 'big') - if self.depth is not None: - map[3] = self.depth - return map - - @classmethod - def from_data_item(cls, item): - map = cls.mapping(item) - path_components = [] - components = map[1] - if components: - for i in range(0, len(components), 2): - hardened = components[i + 1] - path = components[i] - if isinstance(path, int): - path_components.append(PathComponent(path, hardened)) - else: - path_components.append(PathComponent(None, hardened)) - source_fingerprint = map[2].to_bytes(4, 'big') if 2 in map else None - depth = map[3] if 3 in map else None - return cls(path_components, source_fingerprint, depth) + def __init__(self, components, source_fingerprint, depth): + super().__init__() + self.components = components + self.source_fingerprint = source_fingerprint + self.depth = depth + + def __eq__(self, o): + return ( + self.components == o.components + and self.source_fingerprint == o.source_fingerprint + and self.depth == o.depth + ) + + @classmethod + def registry_type(cls): + return CRYPTO_KEYPATH + + def path(self): + if not self.components: + return "" + return "/".join( + [ + ("*" if component.wildcard else str(component.index)) + + ("'" if component.hardened else "") + for component in self.components + ] + ) + + def to_data_item(self): + map = {} + components = [] + for component in self.components: + if component.wildcard: + components.append([]) + else: + components.append(component.index) + components.append(component.hardened) + map[1] = components + if self.source_fingerprint is not None: + map[2] = int.from_bytes(self.source_fingerprint, "big") + if self.depth is not None: + map[3] = self.depth + return map + + @classmethod + def from_data_item(cls, item): + map = cls.mapping(item) + path_components = [] + components = map[1] + if components: + for i in range(0, len(components), 2): + hardened = components[i + 1] + path = components[i] + if isinstance(path, int): + path_components.append(PathComponent(path, hardened)) + else: + path_components.append(PathComponent(None, hardened)) + source_fingerprint = map[2].to_bytes(4, "big") if 2 in map else None + depth = map[3] if 3 in map else None + return cls(path_components, source_fingerprint, depth) + class PathComponent: - def __init__(self, index, hardened): - self.index = index - self.hardened = hardened - self.wildcard = self.index is None - if self.index and self.index & 0x80000000 != 0: - raise ValueError('Invalid index - most significant bit cannot be set') - - def __eq__(self, o): - return self.index == o.index and self.hardened == o.hardened and self.wildcard == o.wildcard \ No newline at end of file + def __init__(self, index, hardened): + self.index = index + self.hardened = hardened + self.wildcard = self.index is None + if self.index and self.index & 0x80000000 != 0: + raise ValueError("Invalid index - most significant bit cannot be set") + + def __eq__(self, o): + return ( + self.index == o.index + and self.hardened == o.hardened + and self.wildcard == o.wildcard + ) diff --git a/src/urtypes/crypto/multi_key.py b/src/urtypes/crypto/multi_key.py index e2aa7dc..e95ea27 100644 --- a/src/urtypes/crypto/multi_key.py +++ b/src/urtypes/crypto/multi_key.py @@ -25,40 +25,45 @@ from .hd_key import HDKey, CRYPTO_HDKEY from .ec_key import ECKey, CRYPTO_ECKEY + class MultiKey(RegistryItem): - def __init__(self, threshold, ec_keys, hd_keys): - super().__init__() - self.threshold = threshold - self.ec_keys = ec_keys - self.hd_keys = hd_keys - - def __eq__(self, o): - return self.threshold == o.threshold and self.ec_keys == o.ec_keys and self.hd_keys == o.hd_keys - - @classmethod - def registry_type(cls): - return None - - def to_data_item(self): - map = {} - map[1] = self.threshold - combined_keys = self.ec_keys[:] + self.hd_keys[:] - keys = [] - for key in combined_keys: - keys.append(DataItem(key.registry_type().tag, key.to_data_item())) - map[2] = keys - return map - - @classmethod - def from_data_item(cls, item): - map = item.map - threshold = map[1] - keys = map[2] - ec_keys = [] - hd_keys = [] - for key in keys: - if key.tag == CRYPTO_HDKEY.tag: - hd_keys.append(HDKey.from_data_item(key)) - elif key.tag == CRYPTO_ECKEY.tag: - ec_keys.append(ECKey.from_data_item(key)) - return cls(threshold, ec_keys, hd_keys) \ No newline at end of file + def __init__(self, threshold, ec_keys, hd_keys): + super().__init__() + self.threshold = threshold + self.ec_keys = ec_keys + self.hd_keys = hd_keys + + def __eq__(self, o): + return ( + self.threshold == o.threshold + and self.ec_keys == o.ec_keys + and self.hd_keys == o.hd_keys + ) + + @classmethod + def registry_type(cls): + return None + + def to_data_item(self): + map = {} + map[1] = self.threshold + combined_keys = self.ec_keys[:] + self.hd_keys[:] + keys = [] + for key in combined_keys: + keys.append(DataItem(key.registry_type().tag, key.to_data_item())) + map[2] = keys + return map + + @classmethod + def from_data_item(cls, item): + map = item.map + threshold = map[1] + keys = map[2] + ec_keys = [] + hd_keys = [] + for key in keys: + if key.tag == CRYPTO_HDKEY.tag: + hd_keys.append(HDKey.from_data_item(key)) + elif key.tag == CRYPTO_ECKEY.tag: + ec_keys.append(ECKey.from_data_item(key)) + return cls(threshold, ec_keys, hd_keys) diff --git a/src/urtypes/crypto/output.py b/src/urtypes/crypto/output.py index aaac8f3..4126e5a 100644 --- a/src/urtypes/crypto/output.py +++ b/src/urtypes/crypto/output.py @@ -27,152 +27,174 @@ from .hd_key import HDKey, CRYPTO_HDKEY from .ec_key import ECKey + class ScriptExpression: - def __init__(self, tag, expression): - self.tag = tag - self.expression = expression - - def __eq__(self, o): - return self.tag == o.tag and self.expression == o.expression - + def __init__(self, tag, expression): + self.tag = tag + self.expression = expression + + def __eq__(self, o): + return self.tag == o.tag and self.expression == o.expression + + SCRIPT_EXPRESSION_TAG_MAP = { - 307: ScriptExpression(307, 'addr'), - 400: ScriptExpression(400, 'sh'), - 401: ScriptExpression(401, 'wsh'), - 402: ScriptExpression(402, 'pk'), - 403: ScriptExpression(403, 'pkh'), - 404: ScriptExpression(404, 'wpkh'), - 405: ScriptExpression(405, 'combo'), - 406: ScriptExpression(406, 'multi'), - 407: ScriptExpression(407, 'sortedmulti'), - 408: ScriptExpression(408, 'raw'), - 409: ScriptExpression(409, 'tr'), - 410: ScriptExpression(410, 'cosigner') + 307: ScriptExpression(307, "addr"), + 400: ScriptExpression(400, "sh"), + 401: ScriptExpression(401, "wsh"), + 402: ScriptExpression(402, "pk"), + 403: ScriptExpression(403, "pkh"), + 404: ScriptExpression(404, "wpkh"), + 405: ScriptExpression(405, "combo"), + 406: ScriptExpression(406, "multi"), + 407: ScriptExpression(407, "sortedmulti"), + 408: ScriptExpression(408, "raw"), + 409: ScriptExpression(409, "tr"), + 410: ScriptExpression(410, "cosigner"), } -CRYPTO_OUTPUT = RegistryType('crypto-output', 308) +CRYPTO_OUTPUT = RegistryType("crypto-output", 308) + class Output(RegistryItem): - def __init__(self, script_expressions, crypto_key): - super().__init__() - self.script_expressions = script_expressions - self.crypto_key = crypto_key - - def __eq__(self, o): - return self.script_expressions == o.script_expressions and self.crypto_key == o.crypto_key - - @classmethod - def registry_type(cls): - return CRYPTO_OUTPUT - - def descriptor(self, include_checksum=True): - descriptor = io.StringIO() - - for script_expression in self.script_expressions: - descriptor.write(script_expression.expression + '(') - - if isinstance(self.crypto_key, MultiKey): - descriptor.write(str(self.crypto_key.threshold) + ',') - - keys = self.crypto_key.ec_keys[:] + self.crypto_key.hd_keys[:] if isinstance(self.crypto_key, MultiKey) else [self.crypto_key] - descriptor.write(','.join([key.descriptor_key() for key in keys])) - - for _ in self.script_expressions: - descriptor.write(')') - - d = descriptor.getvalue() - descriptor.close() - - if include_checksum: - return d + '#' + descriptor_checksum(d) - return d - - def hd_key(self): - if isinstance(self.crypto_key, HDKey): - return self.crypto_key - return None - - def ec_key(self): - if isinstance(self.crypto_key, ECKey): - return self.crypto_key - return None - - def multi_key(self): - if isinstance(self.crypto_key, MultiKey): - return self.crypto_key - return None - - def to_data_item(self): - item = DataItem(None, self.crypto_key.to_data_item()) - if self.crypto_key.registry_type() is not None: - item.tag = self.crypto_key.registry_type().tag - i = len(self.script_expressions) - 1 - while i >= 0: - expression = self.script_expressions[i] - if item.tag is None: - item.tag = expression.tag - else: - item = DataItem(expression.tag, item) - i -= 1 - return item - - @classmethod - def from_data_item(cls, item): - tmp_item = cls.mapping(item) - script_expressions = [] - while True: - tag = tmp_item.tag - if tag in SCRIPT_EXPRESSION_TAG_MAP: - script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[tag]) - if isinstance(tmp_item.map, DataItem): - tmp_item = tmp_item.map - else: - break - else: - break - exp_len = len(script_expressions) - is_multi_key = exp_len > 0 and (script_expressions[exp_len-1].expression == 'multi' or script_expressions[exp_len-1].expression == 'sortedmulti') - if is_multi_key: - return cls(script_expressions, MultiKey.from_data_item(tmp_item)) - - if tmp_item.tag == CRYPTO_HDKEY.tag: - return cls(script_expressions, HDKey.from_data_item(tmp_item)) - else: - return cls(script_expressions, ECKey.from_data_item(tmp_item)) + def __init__(self, script_expressions, crypto_key): + super().__init__() + self.script_expressions = script_expressions + self.crypto_key = crypto_key + + def __eq__(self, o): + return ( + self.script_expressions == o.script_expressions + and self.crypto_key == o.crypto_key + ) + + @classmethod + def registry_type(cls): + return CRYPTO_OUTPUT + + def descriptor(self, include_checksum=True): + descriptor = io.StringIO() + + for script_expression in self.script_expressions: + descriptor.write(script_expression.expression + "(") + + if isinstance(self.crypto_key, MultiKey): + descriptor.write(str(self.crypto_key.threshold) + ",") + + keys = ( + self.crypto_key.ec_keys[:] + self.crypto_key.hd_keys[:] + if isinstance(self.crypto_key, MultiKey) + else [self.crypto_key] + ) + descriptor.write(",".join([key.descriptor_key() for key in keys])) + + for _ in self.script_expressions: + descriptor.write(")") + + d = descriptor.getvalue() + descriptor.close() + + if include_checksum: + return d + "#" + descriptor_checksum(d) + return d + + def hd_key(self): + if isinstance(self.crypto_key, HDKey): + return self.crypto_key + return None + + def ec_key(self): + if isinstance(self.crypto_key, ECKey): + return self.crypto_key + return None + + def multi_key(self): + if isinstance(self.crypto_key, MultiKey): + return self.crypto_key + return None + + def to_data_item(self): + item = DataItem(None, self.crypto_key.to_data_item()) + if self.crypto_key.registry_type() is not None: + item.tag = self.crypto_key.registry_type().tag + i = len(self.script_expressions) - 1 + while i >= 0: + expression = self.script_expressions[i] + if item.tag is None: + item.tag = expression.tag + else: + item = DataItem(expression.tag, item) + i -= 1 + return item + + @classmethod + def from_data_item(cls, item): + tmp_item = cls.mapping(item) + script_expressions = [] + while True: + tag = tmp_item.tag + if tag in SCRIPT_EXPRESSION_TAG_MAP: + script_expressions.append(SCRIPT_EXPRESSION_TAG_MAP[tag]) + if isinstance(tmp_item.map, DataItem): + tmp_item = tmp_item.map + else: + break + else: + break + exp_len = len(script_expressions) + is_multi_key = exp_len > 0 and ( + script_expressions[exp_len - 1].expression == "multi" + or script_expressions[exp_len - 1].expression == "sortedmulti" + ) + if is_multi_key: + return cls(script_expressions, MultiKey.from_data_item(tmp_item)) + + if tmp_item.tag == CRYPTO_HDKEY.tag: + return cls(script_expressions, HDKey.from_data_item(tmp_item)) + else: + return cls(script_expressions, ECKey.from_data_item(tmp_item)) + def polymod(c, val): - c0 = c >> 35 - c = ((c & 0x7ffffffff) << 5) ^ val - if c0 & 1: c ^= 0xf5dee51989 - if c0 & 2: c ^= 0xa9fdca3312 - if c0 & 4: c ^= 0x1bab10e32d - if c0 & 8: c ^= 0x3706b1677a - if c0 & 16: c ^= 0x644d626ffd - return c + c0 = c >> 35 + c = ((c & 0x7FFFFFFFF) << 5) ^ val + if c0 & 1: + c ^= 0xF5DEE51989 + if c0 & 2: + c ^= 0xA9FDCA3312 + if c0 & 4: + c ^= 0x1BAB10E32D + if c0 & 8: + c ^= 0x3706B1677A + if c0 & 16: + c ^= 0x644D626FFD + return c + INPUT_CHARSET = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ " CHECKSUM_CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + def descriptor_checksum(descriptor): - c = 1 - cls = 0 - clscount = 0 - for ch in descriptor: - pos = INPUT_CHARSET.find(ch) - if pos == -1: - return '' - c = polymod(c, pos & 31) - cls = cls * 3 + (pos >> 5) - clscount += 1 - if (clscount == 3): - c = polymod(c, cls) - cls = 0 - clscount = 0 - if clscount > 0: c = polymod(c, cls) - for _ in range(8): - c = polymod(c, 0) - c ^= 1 - checksum = '' - for i in range(8): - checksum += CHECKSUM_CHARSET[(c >> (5 * (7 - i))) & 31] - return checksum + c = 1 + cls = 0 + clscount = 0 + for ch in descriptor: + pos = INPUT_CHARSET.find(ch) + if pos == -1: + return "" + c = polymod(c, pos & 31) + cls = cls * 3 + (pos >> 5) + clscount += 1 + if clscount == 3: + c = polymod(c, cls) + cls = 0 + clscount = 0 + if clscount > 0: + c = polymod(c, cls) + for _ in range(8): + c = polymod(c, 0) + c ^= 1 + checksum = "" + for i in range(8): + checksum += CHECKSUM_CHARSET[(c >> (5 * (7 - i))) & 31] + return checksum diff --git a/src/urtypes/crypto/psbt.py b/src/urtypes/crypto/psbt.py index e0bc3af..cb23408 100644 --- a/src/urtypes/crypto/psbt.py +++ b/src/urtypes/crypto/psbt.py @@ -22,9 +22,10 @@ from urtypes import RegistryType, Bytes -CRYPTO_PSBT = RegistryType('crypto-psbt', 310) +CRYPTO_PSBT = RegistryType("crypto-psbt", 310) + class PSBT(Bytes): - @classmethod - def registry_type(cls): - return CRYPTO_PSBT \ No newline at end of file + @classmethod + def registry_type(cls): + return CRYPTO_PSBT diff --git a/src/urtypes/registry.py b/src/urtypes/registry.py index 2822b77..64fc8c2 100644 --- a/src/urtypes/registry.py +++ b/src/urtypes/registry.py @@ -23,39 +23,43 @@ import io from urtypes.cbor import decoder, encoder, DataItem + class RegistryType: - def __init__(self, type, tag): - self.type = type - self.tag = tag - + def __init__(self, type, tag): + self.type = type + self.tag = tag + + class RegistryItem: - @classmethod - def registry_type(cls): - raise NotImplementedError() - - @classmethod - def mapping(cls, item): - if isinstance(item, DataItem): - registry_type = cls.registry_type() - if (registry_type is None and item.tag is None) or (registry_type is not None and registry_type.tag == item.tag): - return item.map - return item - - @classmethod - def from_data_item(cls, item): - raise NotImplementedError() - - def to_data_item(self): - raise NotImplementedError() - - @classmethod - def from_cbor(cls, cbor_payload): - cbor_decoder = decoder.Decoder(io.BytesIO(cbor_payload)) - return cls.from_data_item(cbor_decoder.decode()) - - def to_cbor(self): - cbor_encoder = encoder.Encoder(io.BytesIO()) - cbor_encoder.encode(self.to_data_item()) - v = cbor_encoder.output.getvalue() - cbor_encoder.output.close() - return bytearray(v) + @classmethod + def registry_type(cls): + raise NotImplementedError() + + @classmethod + def mapping(cls, item): + if isinstance(item, DataItem): + registry_type = cls.registry_type() + if (registry_type is None and item.tag is None) or ( + registry_type is not None and registry_type.tag == item.tag + ): + return item.map + return item + + @classmethod + def from_data_item(cls, item): + raise NotImplementedError() + + def to_data_item(self): + raise NotImplementedError() + + @classmethod + def from_cbor(cls, cbor_payload): + cbor_decoder = decoder.Decoder(io.BytesIO(cbor_payload)) + return cls.from_data_item(cbor_decoder.decode()) + + def to_cbor(self): + cbor_encoder = encoder.Encoder(io.BytesIO()) + cbor_encoder.encode(self.to_data_item()) + v = cbor_encoder.output.getvalue() + cbor_encoder.output.close() + return bytearray(v) diff --git a/tests/crypto/__init__.py b/tests/crypto/__init__.py index 1e16095..3bec148 100644 --- a/tests/crypto/__init__.py +++ b/tests/crypto/__init__.py @@ -18,4 +18,4 @@ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. \ No newline at end of file +# THE SOFTWARE. diff --git a/tests/crypto/test_account.py b/tests/crypto/test_account.py index 98306d6..65cab5e 100644 --- a/tests/crypto/test_account.py +++ b/tests/crypto/test_account.py @@ -22,128 +22,188 @@ import binascii from unittest import TestCase -from urtypes.crypto import Account, Output, SCRIPT_EXPRESSION_TAG_MAP, HDKey, Keypath, PathComponent +from urtypes.crypto import ( + Account, + Output, + SCRIPT_EXPRESSION_TAG_MAP, + HDKey, + Keypath, + PathComponent, +) + class AccountTestCase(TestCase): - def table(self): - return [ - { - 'test': 'Example/Test Vector (#0 account for BTC mainnet for the following BIP39 seed: shield group erode awake lock sausage cash glare wave crew flame glove)', - 'item': Account( - binascii.unhexlify('37b5eed4'), + def table(self): + return [ + { + "test": "Example/Test Vector (#0 account for BTC mainnet for the following BIP39 seed: shield group erode awake lock sausage cash glare wave crew flame glove)", + "item": Account( + binascii.unhexlify("37b5eed4"), [ Output( [SCRIPT_EXPRESSION_TAG_MAP[403]], - HDKey({ - 'key': binascii.unhexlify('03eb3e2863911826374de86c231a4b76f0b89dfa174afb78d7f478199884d9dd32'), - 'chain_code': binascii.unhexlify('6456a5df2db0f6d9af72b2a1af4b25f45200ed6fcc29c3440b311d4796b70b5b'), - 'origin': Keypath( - [ - PathComponent(44, True), - PathComponent(0, True), - PathComponent(0, True) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('99f9cdf7') - }) + HDKey( + { + "key": binascii.unhexlify( + "03eb3e2863911826374de86c231a4b76f0b89dfa174afb78d7f478199884d9dd32" + ), + "chain_code": binascii.unhexlify( + "6456a5df2db0f6d9af72b2a1af4b25f45200ed6fcc29c3440b311d4796b70b5b" + ), + "origin": Keypath( + [ + PathComponent(44, True), + PathComponent(0, True), + PathComponent(0, True), + ], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify( + "99f9cdf7" + ), + } + ), ), Output( - [SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]], - HDKey({ - 'key': binascii.unhexlify('02c7e4823730f6ee2cf864e2c352060a88e60b51a84e89e4c8c75ec22590ad6b69'), - 'chain_code': binascii.unhexlify('9d2f86043276f9251a4a4f577166a5abeb16b6ec61e226b5b8fa11038bfda42d'), - 'origin': Keypath( - [ - PathComponent(49, True), - PathComponent(0, True), - PathComponent(0, True) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('a80f7cdb') - }) + [ + SCRIPT_EXPRESSION_TAG_MAP[400], + SCRIPT_EXPRESSION_TAG_MAP[404], + ], + HDKey( + { + "key": binascii.unhexlify( + "02c7e4823730f6ee2cf864e2c352060a88e60b51a84e89e4c8c75ec22590ad6b69" + ), + "chain_code": binascii.unhexlify( + "9d2f86043276f9251a4a4f577166a5abeb16b6ec61e226b5b8fa11038bfda42d" + ), + "origin": Keypath( + [ + PathComponent(49, True), + PathComponent(0, True), + PathComponent(0, True), + ], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify( + "a80f7cdb" + ), + } + ), ), Output( [SCRIPT_EXPRESSION_TAG_MAP[404]], - HDKey({ - 'key': binascii.unhexlify('03fd433450b6924b4f7efdd5d1ed017d364be95ab2b592dc8bddb3b00c1c24f63f'), - 'chain_code': binascii.unhexlify('72ede7334d5acf91c6fda622c205199c595a31f9218ed30792d301d5ee9e3a88'), - 'origin': Keypath( - [ - PathComponent(84, True), - PathComponent(0, True), - PathComponent(0, True) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('0d5de1d7') - }) + HDKey( + { + "key": binascii.unhexlify( + "03fd433450b6924b4f7efdd5d1ed017d364be95ab2b592dc8bddb3b00c1c24f63f" + ), + "chain_code": binascii.unhexlify( + "72ede7334d5acf91c6fda622c205199c595a31f9218ed30792d301d5ee9e3a88" + ), + "origin": Keypath( + [ + PathComponent(84, True), + PathComponent(0, True), + PathComponent(0, True), + ], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify( + "0d5de1d7" + ), + } + ), ), Output( [SCRIPT_EXPRESSION_TAG_MAP[400]], - HDKey({ - 'key': binascii.unhexlify('035ccd58b63a2cdc23d0812710603592e7457573211880cb59b1ef012e168e059a'), - 'chain_code': binascii.unhexlify('88d3299b448f87215d96b0c226235afc027f9e7dc700284f3e912a34daeb1a23'), - 'origin': Keypath( - [ - PathComponent(45, True) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('37b5eed4') - }) + HDKey( + { + "key": binascii.unhexlify( + "035ccd58b63a2cdc23d0812710603592e7457573211880cb59b1ef012e168e059a" + ), + "chain_code": binascii.unhexlify( + "88d3299b448f87215d96b0c226235afc027f9e7dc700284f3e912a34daeb1a23" + ), + "origin": Keypath( + [PathComponent(45, True)], None, None + ), + "parent_fingerprint": binascii.unhexlify( + "37b5eed4" + ), + } + ), ), Output( - [SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[401]], - HDKey({ - 'key': binascii.unhexlify('032c78ebfcabdac6d735a0820ef8732f2821b4fb84cd5d6b26526938f90c050711'), - 'chain_code': binascii.unhexlify('7953efe16a73e5d3f9f2d4c6e49bd88e22093bbd85be5a7e862a4b98a16e0ab6'), - 'origin': Keypath( - [ - PathComponent(48, True), - PathComponent(0, True), - PathComponent(0, True), - PathComponent(1, True) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('59b69b2a') - }) + [ + SCRIPT_EXPRESSION_TAG_MAP[400], + SCRIPT_EXPRESSION_TAG_MAP[401], + ], + HDKey( + { + "key": binascii.unhexlify( + "032c78ebfcabdac6d735a0820ef8732f2821b4fb84cd5d6b26526938f90c050711" + ), + "chain_code": binascii.unhexlify( + "7953efe16a73e5d3f9f2d4c6e49bd88e22093bbd85be5a7e862a4b98a16e0ab6" + ), + "origin": Keypath( + [ + PathComponent(48, True), + PathComponent(0, True), + PathComponent(0, True), + PathComponent(1, True), + ], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify( + "59b69b2a" + ), + } + ), ), Output( [SCRIPT_EXPRESSION_TAG_MAP[401]], - HDKey({ - 'key': binascii.unhexlify('0260563ee80c26844621b06b74070baf0e23fb76ce439d0237e87502ebbd3ca346'), - 'chain_code': binascii.unhexlify('2fa0e41c9dc43dc4518659bfcef935ba8101b57dbc0812805dd983bc1d34b813'), - 'origin': Keypath( - [ - PathComponent(48, True), - PathComponent(0, True), - PathComponent(0, True), - PathComponent(2, True) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('59b69b2a') - }) - ) - ] + HDKey( + { + "key": binascii.unhexlify( + "0260563ee80c26844621b06b74070baf0e23fb76ce439d0237e87502ebbd3ca346" + ), + "chain_code": binascii.unhexlify( + "2fa0e41c9dc43dc4518659bfcef935ba8101b57dbc0812805dd983bc1d34b813" + ), + "origin": Keypath( + [ + PathComponent(48, True), + PathComponent(0, True), + PathComponent(0, True), + PathComponent(2, True), + ], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify( + "59b69b2a" + ), + } + ), + ), + ], + ), + "cbor": binascii.unhexlify( + "a2011a37b5eed40286d90193d9012fa403582103eb3e2863911826374de86c231a4b76f0b89dfa174afb78d7f478199884d9dd320458206456a5df2db0f6d9af72b2a1af4b25f45200ed6fcc29c3440b311d4796b70b5b06d90130a10186182cf500f500f5081a99f9cdf7d90190d90194d9012fa403582102c7e4823730f6ee2cf864e2c352060a88e60b51a84e89e4c8c75ec22590ad6b690458209d2f86043276f9251a4a4f577166a5abeb16b6ec61e226b5b8fa11038bfda42d06d90130a101861831f500f500f5081aa80f7cdbd90194d9012fa403582103fd433450b6924b4f7efdd5d1ed017d364be95ab2b592dc8bddb3b00c1c24f63f04582072ede7334d5acf91c6fda622c205199c595a31f9218ed30792d301d5ee9e3a8806d90130a101861854f500f500f5081a0d5de1d7d90190d9012fa4035821035ccd58b63a2cdc23d0812710603592e7457573211880cb59b1ef012e168e059a04582088d3299b448f87215d96b0c226235afc027f9e7dc700284f3e912a34daeb1a2306d90130a10182182df5081a37b5eed4d90190d90191d9012fa4035821032c78ebfcabdac6d735a0820ef8732f2821b4fb84cd5d6b26526938f90c0507110458207953efe16a73e5d3f9f2d4c6e49bd88e22093bbd85be5a7e862a4b98a16e0ab606d90130a101881830f500f500f501f5081a59b69b2ad90191d9012fa40358210260563ee80c26844621b06b74070baf0e23fb76ce439d0237e87502ebbd3ca3460458202fa0e41c9dc43dc4518659bfcef935ba8101b57dbc0812805dd983bc1d34b81306d90130a101881830f500f500f502f5081a59b69b2a" ), - 'cbor': binascii.unhexlify('a2011a37b5eed40286d90193d9012fa403582103eb3e2863911826374de86c231a4b76f0b89dfa174afb78d7f478199884d9dd320458206456a5df2db0f6d9af72b2a1af4b25f45200ed6fcc29c3440b311d4796b70b5b06d90130a10186182cf500f500f5081a99f9cdf7d90190d90194d9012fa403582102c7e4823730f6ee2cf864e2c352060a88e60b51a84e89e4c8c75ec22590ad6b690458209d2f86043276f9251a4a4f577166a5abeb16b6ec61e226b5b8fa11038bfda42d06d90130a101861831f500f500f5081aa80f7cdbd90194d9012fa403582103fd433450b6924b4f7efdd5d1ed017d364be95ab2b592dc8bddb3b00c1c24f63f04582072ede7334d5acf91c6fda622c205199c595a31f9218ed30792d301d5ee9e3a8806d90130a101861854f500f500f5081a0d5de1d7d90190d9012fa4035821035ccd58b63a2cdc23d0812710603592e7457573211880cb59b1ef012e168e059a04582088d3299b448f87215d96b0c226235afc027f9e7dc700284f3e912a34daeb1a2306d90130a10182182df5081a37b5eed4d90190d90191d9012fa4035821032c78ebfcabdac6d735a0820ef8732f2821b4fb84cd5d6b26526938f90c0507110458207953efe16a73e5d3f9f2d4c6e49bd88e22093bbd85be5a7e862a4b98a16e0ab606d90130a101881830f500f500f501f5081a59b69b2ad90191d9012fa40358210260563ee80c26844621b06b74070baf0e23fb76ce439d0237e87502ebbd3ca3460458202fa0e41c9dc43dc4518659bfcef935ba8101b57dbc0812805dd983bc1d34b81306d90130a101881830f500f500f502f5081a59b69b2a'), - } - ] + } + ] - def test_from_cbor(self): - for row in self.table(): - self.assertEqual(Account.from_cbor(row['cbor']), row['item']) + def test_from_cbor(self): + for row in self.table(): + self.assertEqual(Account.from_cbor(row["cbor"]), row["item"]) - def test_to_cbor(self): - for row in self.table(): - self.assertEqual(row['item'].to_cbor(), row['cbor']) \ No newline at end of file + def test_to_cbor(self): + for row in self.table(): + self.assertEqual(row["item"].to_cbor(), row["cbor"]) diff --git a/tests/crypto/test_bip39.py b/tests/crypto/test_bip39.py index 7dd4646..fd4bfa2 100644 --- a/tests/crypto/test_bip39.py +++ b/tests/crypto/test_bip39.py @@ -24,23 +24,39 @@ from unittest import TestCase from urtypes.crypto import BIP39 + class BIP39TestCase(TestCase): - def table(self): - return [ - { - 'test': 'Example/Test Vector (16 byte (128-bit) seed, encoded as BIP39)', - 'item': BIP39( - ['shield', 'group', 'erode', 'awake', 'lock', 'sausage', 'cash', 'glare', 'wave', 'crew', 'flame', 'glove'], - 'en' + def table(self): + return [ + { + "test": "Example/Test Vector (16 byte (128-bit) seed, encoded as BIP39)", + "item": BIP39( + [ + "shield", + "group", + "erode", + "awake", + "lock", + "sausage", + "cash", + "glare", + "wave", + "crew", + "flame", + "glove", + ], + "en", + ), + "cbor": binascii.unhexlify( + "a2018c66736869656c646567726f75706565726f6465656177616b65646c6f636b6773617573616765646361736865676c6172656477617665646372657765666c616d6565676c6f76650262656e" ), - 'cbor': binascii.unhexlify('a2018c66736869656c646567726f75706565726f6465656177616b65646c6f636b6773617573616765646361736865676c6172656477617665646372657765666c616d6565676c6f76650262656e'), - } - ] + } + ] - def test_from_cbor(self): - for row in self.table(): - self.assertEqual(BIP39.from_cbor(row['cbor']), row['item']) + def test_from_cbor(self): + for row in self.table(): + self.assertEqual(BIP39.from_cbor(row["cbor"]), row["item"]) - def test_to_cbor(self): - for row in self.table(): - self.assertEqual(row['item'].to_cbor(), row['cbor']) \ No newline at end of file + def test_to_cbor(self): + for row in self.table(): + self.assertEqual(row["item"].to_cbor(), row["cbor"]) diff --git a/tests/crypto/test_ec_key.py b/tests/crypto/test_ec_key.py index 6a2f8a3..9b2f0e1 100644 --- a/tests/crypto/test_ec_key.py +++ b/tests/crypto/test_ec_key.py @@ -24,33 +24,42 @@ from unittest import TestCase from urtypes.crypto import ECKey + class ECKeyTestCase(TestCase): - def table(self): - return [ - { - 'test': 'Example/Test Vector 1 (private key)', - 'item': ECKey( - binascii.unhexlify('8c05c4b4f3e88840a4f4b5f155cfd69473ea169f3d0431b7a6787a23777f08aa'), - None, - True + def table(self): + return [ + { + "test": "Example/Test Vector 1 (private key)", + "item": ECKey( + binascii.unhexlify( + "8c05c4b4f3e88840a4f4b5f155cfd69473ea169f3d0431b7a6787a23777f08aa" + ), + None, + True, + ), + "cbor": binascii.unhexlify( + "a202f50358208c05c4b4f3e88840a4f4b5f155cfd69473ea169f3d0431b7a6787a23777f08aa" + ), + }, + { + "test": "Example/Test Vector 1 (public key)", + "item": ECKey( + binascii.unhexlify( + "03bec5163df25d8703150c3a1804eac7d615bb212b7cc9d7ff937aa8bd1c494b7f" + ), + None, + None, ), - 'cbor': binascii.unhexlify('a202f50358208c05c4b4f3e88840a4f4b5f155cfd69473ea169f3d0431b7a6787a23777f08aa'), - }, - { - 'test': 'Example/Test Vector 1 (public key)', - 'item': ECKey( - binascii.unhexlify('03bec5163df25d8703150c3a1804eac7d615bb212b7cc9d7ff937aa8bd1c494b7f'), - None, - None + "cbor": binascii.unhexlify( + "a103582103bec5163df25d8703150c3a1804eac7d615bb212b7cc9d7ff937aa8bd1c494b7f" ), - 'cbor': binascii.unhexlify('a103582103bec5163df25d8703150c3a1804eac7d615bb212b7cc9d7ff937aa8bd1c494b7f'), - } - ] + }, + ] - def test_from_cbor(self): - for row in self.table(): - self.assertEqual(ECKey.from_cbor(row['cbor']), row['item']) + def test_from_cbor(self): + for row in self.table(): + self.assertEqual(ECKey.from_cbor(row["cbor"]), row["item"]) - def test_to_cbor(self): - for row in self.table(): - self.assertEqual(row['item'].to_cbor(), row['cbor']) \ No newline at end of file + def test_to_cbor(self): + for row in self.table(): + self.assertEqual(row["item"].to_cbor(), row["cbor"]) diff --git a/tests/crypto/test_hd_key.py b/tests/crypto/test_hd_key.py index 545482d..02b8631 100644 --- a/tests/crypto/test_hd_key.py +++ b/tests/crypto/test_hd_key.py @@ -24,45 +24,62 @@ from unittest import TestCase from urtypes.crypto import HDKey, CoinInfo, Keypath, PathComponent + class HDKeyTestCase(TestCase): - def table(self): - return [ - { - 'test': 'Example/Test Vector 1 (master key)', - 'item': HDKey({ - 'master': True, - 'key': binascii.unhexlify('00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35'), - 'chain_code': binascii.unhexlify('873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508') - }), - 'cbor': binascii.unhexlify('a301f503582100e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35045820873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508'), - }, - { - 'test': 'Example/Test Vector 2 (bitcoin testnet public key with derivation path m/44\'/1\'/1\'/0/1)', - 'item': HDKey({ - 'key': binascii.unhexlify('026fe2355745bb2db3630bbc80ef5d58951c963c841f54170ba6e5c12be7fc12a6'), - 'chain_code': binascii.unhexlify('ced155c72456255881793514edc5bd9447e7f74abb88c6d6b6480fd016ee8c85'), - 'use_info': CoinInfo(None, 1), - 'origin': Keypath( - [ - PathComponent(44, True), - PathComponent(1, True), - PathComponent(1, True), - PathComponent(0, False), - PathComponent(1, False) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('e9181cf3') - }), - 'cbor': binascii.unhexlify('a5035821026fe2355745bb2db3630bbc80ef5d58951c963c841f54170ba6e5c12be7fc12a6045820ced155c72456255881793514edc5bd9447e7f74abb88c6d6b6480fd016ee8c8505d90131a1020106d90130a1018a182cf501f501f500f401f4081ae9181cf3'), - } - ] + def table(self): + return [ + { + "test": "Example/Test Vector 1 (master key)", + "item": HDKey( + { + "master": True, + "key": binascii.unhexlify( + "00e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35" + ), + "chain_code": binascii.unhexlify( + "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" + ), + } + ), + "cbor": binascii.unhexlify( + "a301f503582100e8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35045820873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508" + ), + }, + { + "test": "Example/Test Vector 2 (bitcoin testnet public key with derivation path m/44'/1'/1'/0/1)", + "item": HDKey( + { + "key": binascii.unhexlify( + "026fe2355745bb2db3630bbc80ef5d58951c963c841f54170ba6e5c12be7fc12a6" + ), + "chain_code": binascii.unhexlify( + "ced155c72456255881793514edc5bd9447e7f74abb88c6d6b6480fd016ee8c85" + ), + "use_info": CoinInfo(None, 1), + "origin": Keypath( + [ + PathComponent(44, True), + PathComponent(1, True), + PathComponent(1, True), + PathComponent(0, False), + PathComponent(1, False), + ], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify("e9181cf3"), + } + ), + "cbor": binascii.unhexlify( + "a5035821026fe2355745bb2db3630bbc80ef5d58951c963c841f54170ba6e5c12be7fc12a6045820ced155c72456255881793514edc5bd9447e7f74abb88c6d6b6480fd016ee8c8505d90131a1020106d90130a1018a182cf501f501f500f401f4081ae9181cf3" + ), + }, + ] - def test_from_cbor(self): - for row in self.table(): - self.assertEqual(HDKey.from_cbor(row['cbor']), row['item']) + def test_from_cbor(self): + for row in self.table(): + self.assertEqual(HDKey.from_cbor(row["cbor"]), row["item"]) - def test_to_cbor(self): - for row in self.table(): - self.assertEqual(row['item'].to_cbor(), row['cbor']) \ No newline at end of file + def test_to_cbor(self): + for row in self.table(): + self.assertEqual(row["item"].to_cbor(), row["cbor"]) diff --git a/tests/crypto/test_output.py b/tests/crypto/test_output.py index ff27f8f..73a2d8d 100644 --- a/tests/crypto/test_output.py +++ b/tests/crypto/test_output.py @@ -22,159 +22,209 @@ import binascii from unittest import TestCase -from urtypes.crypto import Keypath, PathComponent, MultiKey, ECKey, HDKey, Output, SCRIPT_EXPRESSION_TAG_MAP +from urtypes.crypto import ( + Keypath, + PathComponent, + MultiKey, + ECKey, + HDKey, + Output, + SCRIPT_EXPRESSION_TAG_MAP, +) + class OutputTestCase(TestCase): - def table(self): - return [ - { - 'test': 'Example/Test Vector 1 (P2PKH output with the specified public key)', - 'item': Output( - [SCRIPT_EXPRESSION_TAG_MAP[403]], - ECKey( - binascii.unhexlify('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'), - None, - None - ) - ), - 'cbor': binascii.unhexlify('d90193d90132a103582102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'), - 'descriptor': 'pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)', - 'descriptor_checksum': '#8fhd9pwu' - }, - { - 'test': 'Example/Test Vector 2 (P2SH-P2WPKH output with the specified public key)', - 'item': Output( - [SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]], - ECKey( - binascii.unhexlify('03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556'), - None, - None - ), - ), - 'cbor': binascii.unhexlify('d90190d90194d90132a103582103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556'), - 'descriptor': 'sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))', - 'descriptor_checksum': '#qkrrc7je' - }, - { - 'test': 'Example/Test Vector 3 (P2SH 2-of-2 multisig output with keys in the specified order)', - 'item': Output( - [SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[406]], - MultiKey( - 2, - [ - ECKey( - binascii.unhexlify('022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01'), - None, - None - ), - ECKey( - binascii.unhexlify('03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe'), - None, - None - ) - ], - [] - ) - ), - 'cbor': binascii.unhexlify('d90190d90196a201020282d90132a1035821022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01d90132a103582103acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe'), - 'descriptor': 'sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))', - 'descriptor_checksum': '#y9zthqta' - }, - { - 'test': 'Example/Test Vector 4 (set of P2PKH outputs derived from this key by /1/*, but additionally specifies that the specified xpub is a child of a master with fingerprint d34db33f, and derived using path 44\'/0\'/0\')', - 'item': Output( - [SCRIPT_EXPRESSION_TAG_MAP[403]], - HDKey({ - 'key': binascii.unhexlify('02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0'), - 'chain_code': binascii.unhexlify('637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29'), - 'origin': Keypath( - [ - PathComponent(44, True), - PathComponent(0, True), - PathComponent(0, True) - ], - binascii.unhexlify('d34db33f'), - None - ), - 'children': Keypath( - [ - PathComponent(1, False), - PathComponent(None, False) - ], - None, - None - ), - 'parent_fingerprint': binascii.unhexlify('78412e3a') - }) - ), - 'cbor': binascii.unhexlify('d90193d9012fa503582102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0045820637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e2906d90130a20186182cf500f500f5021ad34db33f07d90130a1018401f480f4081a78412e3a'), - 'descriptor': 'pkh([d34db33f/44\'/0\'/0\']xpub6CY2xt3mvQejPFUw26CychtL4GMq1yp41aMW2U27mvThqefpZYwXpGscV26JuVj13Fpg4kgSENheUSbTqm5f8z25zrhXpPVss5zWeMGnAKR/1/*)', - 'descriptor_checksum': '#tgg7npaw' - }, - { - 'test': 'Example/Test Vector 5 (set of 1-of-2 P2WSH multisig outputs where the first multisig key is the 1/0/i child of the first specified xpub and the second multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range)', - 'item': Output( - [SCRIPT_EXPRESSION_TAG_MAP[401], SCRIPT_EXPRESSION_TAG_MAP[406]], - MultiKey( - 1, - [], - [ - HDKey({ - 'key': binascii.unhexlify('03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7'), - 'chain_code': binascii.unhexlify('60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689'), - 'origin': Keypath( - [], - None, - 0 - ), - 'children': Keypath( - [ - PathComponent(1, False), - PathComponent(0, False), - PathComponent(None, False) - ], - None, - None - ) - }), - HDKey({ - 'key': binascii.unhexlify('02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea'), - 'chain_code': binascii.unhexlify('f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c'), - 'origin': Keypath( - [ - PathComponent(0, False) - ], - binascii.unhexlify('bd16bee5'), - None - ), - 'children': Keypath( - [ - PathComponent(0, False), - PathComponent(0, False), - PathComponent(None, False) - ], - None, - None - ) - }) - ] - ) - ), - 'cbor': binascii.unhexlify('d90191d90196a201010282d9012fa403582103cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a704582060499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd968906d90130a20180030007d90130a1018601f400f480f4d9012fa403582102fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea045820f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c06d90130a2018200f4021abd16bee507d90130a1018600f400f480f4'), - 'descriptor': 'wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))', - 'descriptor_checksum': '#t2zpj2eu' - } - ] + def table(self): + return [ + { + "test": "Example/Test Vector 1 (P2PKH output with the specified public key)", + "item": Output( + [SCRIPT_EXPRESSION_TAG_MAP[403]], + ECKey( + binascii.unhexlify( + "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + ), + None, + None, + ), + ), + "cbor": binascii.unhexlify( + "d90193d90132a103582102c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" + ), + "descriptor": "pkh(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)", + "descriptor_checksum": "#8fhd9pwu", + }, + { + "test": "Example/Test Vector 2 (P2SH-P2WPKH output with the specified public key)", + "item": Output( + [SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[404]], + ECKey( + binascii.unhexlify( + "03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556" + ), + None, + None, + ), + ), + "cbor": binascii.unhexlify( + "d90190d90194d90132a103582103fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556" + ), + "descriptor": "sh(wpkh(03fff97bd5755eeea420453a14355235d382f6472f8568a18b2f057a1460297556))", + "descriptor_checksum": "#qkrrc7je", + }, + { + "test": "Example/Test Vector 3 (P2SH 2-of-2 multisig output with keys in the specified order)", + "item": Output( + [SCRIPT_EXPRESSION_TAG_MAP[400], SCRIPT_EXPRESSION_TAG_MAP[406]], + MultiKey( + 2, + [ + ECKey( + binascii.unhexlify( + "022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01" + ), + None, + None, + ), + ECKey( + binascii.unhexlify( + "03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe" + ), + None, + None, + ), + ], + [], + ), + ), + "cbor": binascii.unhexlify( + "d90190d90196a201020282d90132a1035821022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01d90132a103582103acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe" + ), + "descriptor": "sh(multi(2,022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01,03acd484e2f0c7f65309ad178a9f559abde09796974c57e714c35f110dfc27ccbe))", + "descriptor_checksum": "#y9zthqta", + }, + { + "test": "Example/Test Vector 4 (set of P2PKH outputs derived from this key by /1/*, but additionally specifies that the specified xpub is a child of a master with fingerprint d34db33f, and derived using path 44'/0'/0')", + "item": Output( + [SCRIPT_EXPRESSION_TAG_MAP[403]], + HDKey( + { + "key": binascii.unhexlify( + "02d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0" + ), + "chain_code": binascii.unhexlify( + "637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e29" + ), + "origin": Keypath( + [ + PathComponent(44, True), + PathComponent(0, True), + PathComponent(0, True), + ], + binascii.unhexlify("d34db33f"), + None, + ), + "children": Keypath( + [PathComponent(1, False), PathComponent(None, False)], + None, + None, + ), + "parent_fingerprint": binascii.unhexlify("78412e3a"), + } + ), + ), + "cbor": binascii.unhexlify( + "d90193d9012fa503582102d2b36900396c9282fa14628566582f206a5dd0bcc8d5e892611806cafb0301f0045820637807030d55d01f9a0cb3a7839515d796bd07706386a6eddf06cc29a65a0e2906d90130a20186182cf500f500f5021ad34db33f07d90130a1018401f480f4081a78412e3a" + ), + "descriptor": "pkh([d34db33f/44'/0'/0']xpub6CY2xt3mvQejPFUw26CychtL4GMq1yp41aMW2U27mvThqefpZYwXpGscV26JuVj13Fpg4kgSENheUSbTqm5f8z25zrhXpPVss5zWeMGnAKR/1/*)", + "descriptor_checksum": "#tgg7npaw", + }, + { + "test": "Example/Test Vector 5 (set of 1-of-2 P2WSH multisig outputs where the first multisig key is the 1/0/i child of the first specified xpub and the second multisig key is the 0/0/i child of the second specified xpub, and i is any number in a configurable range)", + "item": Output( + [SCRIPT_EXPRESSION_TAG_MAP[401], SCRIPT_EXPRESSION_TAG_MAP[406]], + MultiKey( + 1, + [], + [ + HDKey( + { + "key": binascii.unhexlify( + "03cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a7" + ), + "chain_code": binascii.unhexlify( + "60499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd9689" + ), + "origin": Keypath([], None, 0), + "children": Keypath( + [ + PathComponent(1, False), + PathComponent(0, False), + PathComponent(None, False), + ], + None, + None, + ), + } + ), + HDKey( + { + "key": binascii.unhexlify( + "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea" + ), + "chain_code": binascii.unhexlify( + "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c" + ), + "origin": Keypath( + [PathComponent(0, False)], + binascii.unhexlify("bd16bee5"), + None, + ), + "children": Keypath( + [ + PathComponent(0, False), + PathComponent(0, False), + PathComponent(None, False), + ], + None, + None, + ), + } + ), + ], + ), + ), + "cbor": binascii.unhexlify( + "d90191d90196a201010282d9012fa403582103cbcaa9c98c877a26977d00825c956a238e8dddfbd322cce4f74b0b5bd6ace4a704582060499f801b896d83179a4374aeb7822aaeaceaa0db1f85ee3e904c4defbd968906d90130a20180030007d90130a1018601f400f480f4d9012fa403582102fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea045820f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c06d90130a2018200f4021abd16bee507d90130a1018600f400f480f4" + ), + "descriptor": "wsh(multi(1,xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*,xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*))", + "descriptor_checksum": "#t2zpj2eu", + }, + ] + + def test_from_cbor(self): + for row in self.table(): + self.assertEqual( + Output.from_cbor(row["cbor"]), + row["item"], + msg="\nFailed: %s" % row["test"], + ) - def test_from_cbor(self): - for row in self.table(): - self.assertEqual(Output.from_cbor(row['cbor']), row['item'], msg='\nFailed: %s' % row['test']) + def test_to_cbor(self): + for row in self.table(): + self.assertEqual( + row["item"].to_cbor(), row["cbor"], msg="\nFailed: %s" % row["test"] + ) - def test_to_cbor(self): - for row in self.table(): - self.assertEqual(row['item'].to_cbor(), row['cbor'], msg='\nFailed: %s' % row['test']) - - def test_descriptor(self): - for row in self.table(): - self.assertEqual(row['item'].descriptor(False), row['descriptor'], msg='\nFailed: %s' % row['test']) - self.assertEqual(row['item'].descriptor(True), row['descriptor'] + row['descriptor_checksum'], msg='\nFailed: %s' % row['test']) \ No newline at end of file + def test_descriptor(self): + for row in self.table(): + self.assertEqual( + row["item"].descriptor(False), + row["descriptor"], + msg="\nFailed: %s" % row["test"], + ) + self.assertEqual( + row["item"].descriptor(True), + row["descriptor"] + row["descriptor_checksum"], + msg="\nFailed: %s" % row["test"], + ) diff --git a/tests/crypto/test_psbt.py b/tests/crypto/test_psbt.py index aea179d..21eea9d 100644 --- a/tests/crypto/test_psbt.py +++ b/tests/crypto/test_psbt.py @@ -24,20 +24,27 @@ from unittest import TestCase from urtypes.crypto import PSBT + class PSBTTestCase(TestCase): - def table(self): - return [ - { - 'test': 'Example/Test Vector', - 'item': PSBT(binascii.unhexlify('70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000')), - 'cbor': binascii.unhexlify('58A770736274FF01009A020000000258E87A21B56DAF0C23BE8E7070456C336F7CBAA5C8757924F545887BB2ABDD750000000000FFFFFFFF838D0427D0EC650A68AA46BB0B098AEA4422C071B2CA78352A077959D07CEA1D0100000000FFFFFFFF0270AAF00800000000160014D85C2B71D0060B09C9886AEB815E50991DDA124D00E1F5050000000016001400AEA9A2E5F0F876A588DF5546E8742D1D87008F000000000000000000'), - } - ] - - def test_from_cbor(self): - for row in self.table(): - self.assertEqual(PSBT.from_cbor(row['cbor']), row['item']) - - def test_to_cbor(self): - for row in self.table(): - self.assertEqual(row['item'].to_cbor(), row['cbor']) \ No newline at end of file + def table(self): + return [ + { + "test": "Example/Test Vector", + "item": PSBT( + binascii.unhexlify( + "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000000000000000000" + ) + ), + "cbor": binascii.unhexlify( + "58A770736274FF01009A020000000258E87A21B56DAF0C23BE8E7070456C336F7CBAA5C8757924F545887BB2ABDD750000000000FFFFFFFF838D0427D0EC650A68AA46BB0B098AEA4422C071B2CA78352A077959D07CEA1D0100000000FFFFFFFF0270AAF00800000000160014D85C2B71D0060B09C9886AEB815E50991DDA124D00E1F5050000000016001400AEA9A2E5F0F876A588DF5546E8742D1D87008F000000000000000000" + ), + } + ] + + def test_from_cbor(self): + for row in self.table(): + self.assertEqual(PSBT.from_cbor(row["cbor"]), row["item"]) + + def test_to_cbor(self): + for row in self.table(): + self.assertEqual(row["item"].to_cbor(), row["cbor"])