diff --git a/.gitignore b/.gitignore index c462d5a..ded3cbe 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,3 @@ thumbs.db .ruff_cache/ # Generated protobuf files -**/src/**/proto diff --git a/Makefile b/Makefile index 759deae..ac3329e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: setup prebuild test lint format clean help +.PHONY: setup prebuild test lint format clean help test-app # Default target help: @@ -6,6 +6,7 @@ help: @echo " setup - Complete setup (install dependencies and run prebuild)" @echo " prebuild - Run prebuild for all packages" @echo " test - Run all tests" + @echo " test-app - Run test application (interactive)" @echo " lint - Run linting checks" @echo " format - Format code with black" @echo " clean - Clean generated files" @@ -14,7 +15,11 @@ help: setup: @echo "Setting up Cypherock SDK..." @echo "1. Installing dependencies..." - poetry install + @HOMEBREW_PREFIX=$$([ -d "/opt/homebrew" ] && echo "/opt/homebrew" || echo "/usr/local") && \ + export LIBRARY_PATH="$$HOMEBREW_PREFIX/lib:$$LIBRARY_PATH" && \ + export CPPFLAGS="-I$$HOMEBREW_PREFIX/include $$CPPFLAGS" && \ + export LDFLAGS="-L$$HOMEBREW_PREFIX/lib $$LDFLAGS" && \ + poetry install @echo "2. Running prebuild..." $(MAKE) prebuild @echo "Setup complete!" @@ -42,6 +47,15 @@ test: prebuild @echo "Running util package tests..." poetry run pytest packages/util/tests/ -v +# Run test application (for testing with actual firmware) +test-app: prebuild + @echo "Running test application..." + @echo "Use 'poetry run python test_app/main.py --help' for usage" + @HOMEBREW_PREFIX=$$([ -d "/opt/homebrew" ] && echo "/opt/homebrew" || echo "/usr/local") && \ + export DYLD_LIBRARY_PATH="$$HOMEBREW_PREFIX/lib:$$DYLD_LIBRARY_PATH" && \ + export LD_LIBRARY_PATH="$$HOMEBREW_PREFIX/lib:$$LD_LIBRARY_PATH" && \ + poetry run python test_app/main.py --list-devices || true + # Run linting lint: @echo "Running linting checks..." diff --git a/packages/app_btc/.gitignore b/packages/app_btc/.gitignore new file mode 100644 index 0000000..4d46779 --- /dev/null +++ b/packages/app_btc/.gitignore @@ -0,0 +1 @@ +src/app_btc/proto/generated \ No newline at end of file diff --git a/packages/app_btc/poetry.lock b/packages/app_btc/poetry.lock index bbacec6..cd64b03 100644 --- a/packages/app_btc/poetry.lock +++ b/packages/app_btc/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "anyio" @@ -6,6 +6,7 @@ version = "4.10.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, @@ -25,6 +26,7 @@ version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, @@ -36,6 +38,7 @@ version = "2.1.1" description = "Base58 and Base58Check implementation." optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2"}, {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"}, @@ -44,12 +47,25 @@ files = [ [package.extras] tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"] +[[package]] +name = "base58check" +version = "1.0.2" +description = "Base58check encoding and decoding of binary data" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "base58check-1.0.2-py2.py3-none-any.whl", hash = "sha256:a83863696845a9b0b5ec95aebb32ebfc6ce5596984302c0f2b7b27f24afc2719"}, + {file = "base58check-1.0.2.tar.gz", hash = "sha256:a597d250ef564806d5c91a8882c92a69f542b90c6b4f04f581ee3202855cc396"}, +] + [[package]] name = "bech32" version = "1.2.0" description = "Reference implementation for Bech32 and segwit addresses." optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"}, {file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"}, @@ -61,6 +77,7 @@ version = "1.2.5" description = "A better Protobuf / gRPC generator & library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "betterproto-1.2.5.tar.gz", hash = "sha256:74a3ab34646054f674d236d1229ba8182dc2eae86feb249b8590ef496ce9803d"}, ] @@ -72,12 +89,32 @@ stringcase = "*" [package.extras] compiler = ["black", "jinja2", "protobuf"] +[[package]] +name = "bitcoin-utils" +version = "0.7.3" +description = "Bitcoin utility functions" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "bitcoin-utils-0.7.3.tar.gz", hash = "sha256:da086dea8c9cc2dc55a2e3949490623ed98d0b09afb41e469a3b4e10e9af8602"}, + {file = "bitcoin_utils-0.7.3-py3-none-any.whl", hash = "sha256:5590fd9f2a57710c1a2e4f0e5d748dd1bd7ee607df75241ca2d435a00cc2cc50"}, +] + +[package.dependencies] +base58check = ">=1.0.2,<2.0" +ecdsa = ">=0.19.1,<0.20" +hdwallet = ">=3.0,<4.0" +python-bitcoinrpc = ">=1.0,<2.0" +sympy = ">=1.2,<2.0" + [[package]] name = "bitcoinlib" version = "0.7.5" description = "Bitcoin cryptocurrency Library" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "bitcoinlib-0.7.5-py3-none-any.whl", hash = "sha256:3c273f0805d7aa08f292b1d4553c083bf8f1f72f4ec29479df2e05b95965c8d4"}, {file = "bitcoinlib-0.7.5.tar.gz", hash = "sha256:5de91c468e77144d5f3ab530c6a5b44a6e7d5a40970162f00c509861bb640da6"}, @@ -92,7 +129,7 @@ requests = ">=2.25.0" SQLAlchemy = ">=2.0.20" [package.extras] -dev = ["Cython (>=3.0.8)", "coveralls (>=4.0.1)", "mysql-connector-python (>=8.4.0)", "mysqlclient (>=2.2.0)", "psycopg (>=3.1.16)", "scrypt (>=0.8.20)", "sphinx (>=7.1.0)", "sphinx (>=7.2.0)", "sphinx_rtd_theme (>=2.0.0)", "win-unicode-console"] +dev = ["Cython (>=3.0.8)", "coveralls (>=4.0.1)", "mysql-connector-python (>=8.4.0)", "mysqlclient (>=2.2.0)", "psycopg (>=3.1.16)", "scrypt (>=0.8.20) ; platform_system != \"Windows\"", "sphinx (>=7.1.0) ; python_version < \"3.9\"", "sphinx (>=7.2.0) ; python_version >= \"3.9\"", "sphinx_rtd_theme (>=2.0.0)", "win-unicode-console ; platform_system == \"Windows\""] [[package]] name = "black" @@ -100,6 +137,7 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -138,12 +176,67 @@ d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "cbor2" +version = "5.8.0" +description = "CBOR (de)serializer with extensive tag support" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "cbor2-5.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2263c0c892194f10012ced24c322d025d9d7b11b41da1c357f3b3fe06676e6b7"}, + {file = "cbor2-5.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6ffe4ca079f6f8ed393f5c71a8de22651cb27bd50e74e2bcd6bc9c8f853a732b"}, + {file = "cbor2-5.8.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0427bd166230fe4c4b72965c6f2b6273bf29016d97cf08b258fa48db851ea598"}, + {file = "cbor2-5.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c23a04947c37964d70028ca44ea2a8709f09b8adc0090f9b5710fa957e9bc545"}, + {file = "cbor2-5.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:218d5c7d2e8d13c7eded01a1b3fe2a9a1e51a7a843cefb8d38cb4bbbc6ad9bf7"}, + {file = "cbor2-5.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:4ce7d907a25448af7c13415281d739634edfd417228b274309b243ca52ad71f9"}, + {file = "cbor2-5.8.0-cp310-cp310-win_arm64.whl", hash = "sha256:628d0ea850aa040921a0e50a08180e7d20cf691432cec3eabc193f643eccfbde"}, + {file = "cbor2-5.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ac191640093e6c7fbcb174c006ffec4106c3d8ab788e70272c1c4d933cbe11"}, + {file = "cbor2-5.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fddee9103a17d7bed5753f0c7fc6663faa506eb953e50d8287804eccf7b048e6"}, + {file = "cbor2-5.8.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d2ea26fad620aba5e88d7541be8b10c5034a55db9a23809b7cb49f36803f05b"}, + {file = "cbor2-5.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:de68b4b310b072b082d317adc4c5e6910173a6d9455412e6183d72c778d1f54c"}, + {file = "cbor2-5.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:418d2cf0e03e90160fa1474c05a40fe228bbb4a92d1628bdbbd13a48527cb34d"}, + {file = "cbor2-5.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:453200ffa1c285ea46ab5745736a015526d41f22da09cb45594624581d959770"}, + {file = "cbor2-5.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:f6615412fca973a8b472b3efc4dab01df71cc13f15d8b2c0a1cffac44500f12d"}, + {file = "cbor2-5.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b3f91fa699a5ce22470e973601c62dd9d55dc3ca20ee446516ac075fcab27c9"}, + {file = "cbor2-5.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:518c118a5e00001854adb51f3164e647aa99b6a9877d2a733a28cb5c0a4d6857"}, + {file = "cbor2-5.8.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cff2a1999e49cd51c23d1b6786a012127fd8f722c5946e82bd7ab3eb307443f3"}, + {file = "cbor2-5.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c4492160212374973cdc14e46f0565f2462721ef922b40f7ea11e7d613dfb2a"}, + {file = "cbor2-5.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:546c7c7c4c6bcdc54a59242e0e82cea8f332b17b4465ae628718fef1fce401ca"}, + {file = "cbor2-5.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:074f0fa7535dd7fdee247c2c99f679d94f3aa058ccb1ccf4126cc72d6d89cbae"}, + {file = "cbor2-5.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:f95fed480b2a0d843f294d2a1ef4cc0f6a83c7922927f9f558e1f5a8dc54b7ca"}, + {file = "cbor2-5.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d8d104480845e2f28c6165b4c961bbe58d08cb5638f368375cfcae051c28015"}, + {file = "cbor2-5.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:43efee947e5ab67d406d6e0dc61b5dee9d2f5e89ae176f90677a3741a20ca2e7"}, + {file = "cbor2-5.8.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be7ae582f50be539e09c134966d0fd63723fc4789b8dff1f6c2e3f24ae3eaf32"}, + {file = "cbor2-5.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50f5c709561a71ea7970b4cd2bf9eda4eccacc0aac212577080fdfe64183e7f5"}, + {file = "cbor2-5.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a6790ecc73aa93e76d2d9076fc42bf91a9e69f2295e5fa702e776dbe986465bd"}, + {file = "cbor2-5.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:c114af8099fa65a19a514db87ce7a06e942d8fea2730afd49be39f8e16e7f5e0"}, + {file = "cbor2-5.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:ab3ba00494ad8669a459b12a558448d309c271fa4f89b116ad496ee35db38fea"}, + {file = "cbor2-5.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ad72381477133046ce217617d839ea4e9454f8b77d9a6351b229e214102daeb7"}, + {file = "cbor2-5.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6da25190fad3434ce99876b11d4ca6b8828df6ca232cf7344cd14ae1166fb718"}, + {file = "cbor2-5.8.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c13919e3a24c5a6d286551fa288848a4cedc3e507c58a722ccd134e461217d99"}, + {file = "cbor2-5.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f8c40d32e5972047a777f9bf730870828f3cf1c43b3eb96fd0429c57a1d3b9e6"}, + {file = "cbor2-5.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7627894bc0b3d5d0807f31e3107e11b996205470c4429dc2bb4ef8bfe7f64e1e"}, + {file = "cbor2-5.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:b51c5e59becae746ca4de2bbaa8a2f5c64a68fec05cea62941b1a84a8335f7d1"}, + {file = "cbor2-5.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:53b630f4db4b9f477ad84077283dd17ecf9894738aa17ef4938c369958e02a71"}, + {file = "cbor2-5.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6cda8fc407e91c4b07f1ae217332b2418096345b2f003894425bd874af445573"}, + {file = "cbor2-5.8.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f935c87350cfcccfa3499413c90d62d0591c8220932c200c2a7108737d4c96c6"}, + {file = "cbor2-5.8.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f36afff8d8527d68cabf1b13acef15a573c0864b99017e315dcbe5710cb7e6e"}, + {file = "cbor2-5.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9f197a7b33c3afa44f18d16a2f823c1c020e3eb57a79cfaa0f21435e9a7f732"}, + {file = "cbor2-5.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4df31a52b20d28bf60ee35d16b2b43c2870b77c901cbc42e4151b575b20d522e"}, + {file = "cbor2-5.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:88db454bfdfeb7c611b926e70f28d4bc37e7cbc55594141a3514cc087c8890c2"}, + {file = "cbor2-5.8.0-cp39-cp39-win_arm64.whl", hash = "sha256:b0400d2c98b3137448090cd9cfa9d3ecf1b04852328339c85025b1c3acfd8b7d"}, + {file = "cbor2-5.8.0-py3-none-any.whl", hash = "sha256:3727d80f539567b03a7aa11890e57798c67092c38df9e6c23abb059e0f65069c"}, + {file = "cbor2-5.8.0.tar.gz", hash = "sha256:b19c35fcae9688ac01ef75bad5db27300c2537eb4ee00ed07e05d8456a0d4931"}, +] + [[package]] name = "certifi" version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -155,6 +248,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -234,6 +328,7 @@ version = "3.4.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, @@ -322,6 +417,7 @@ version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -336,6 +432,7 @@ version = "20.0.0" description = "Cross-platform Python CFFI bindings for libsecp256k1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "coincurve-20.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d559b22828638390118cae9372a1bb6f6594f5584c311deb1de6a83163a0919b"}, {file = "coincurve-20.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33d7f6ebd90fcc550f819f7f2cce2af525c342aac07f0ccda46ad8956ad9d99b"}, @@ -402,17 +499,31 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "crcmod" +version = "1.7" +description = "CRC Generator" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "crcmod-1.7.tar.gz", hash = "sha256:dc7051a0db5f2bd48665a990d3ec1cc305a466a77358ca4492826f41f283601e"}, +] + [[package]] name = "cryptography" version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, @@ -454,13 +565,14 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "ecdsa" -version = "0.18.0" +version = "0.19.1" description = "ECDSA cryptographic signature library (pure python)" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6" +groups = ["main"] files = [ - {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, - {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, + {file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"}, + {file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"}, ] [package.dependencies] @@ -470,12 +582,25 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] +[[package]] +name = "ed25519-blake2b" +version = "1.4.1" +description = "Ed25519 public-key signatures (BLAKE2b fork)" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ed25519-blake2b-1.4.1.tar.gz", hash = "sha256:731e9f93cd1ac1a64649575f3519a99ffe0bb1e4cf7bf5f5f0be513a39df7363"}, +] + [[package]] name = "fastecdsa" version = "3.0.1" description = "Fast elliptic curve digital signatures" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system != \"Windows\"" files = [ {file = "fastecdsa-3.0.1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:5793ac17891bc8c7002c8364b5df32ac17b486a68c6e32d941f3f0b3641b417b"}, {file = "fastecdsa-3.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e99b2695f3cbf363fa4fc12e036b5a2b50a01004b82972dce102bb73fa1833bd"}, @@ -506,6 +631,8 @@ version = "3.2.4" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, @@ -515,6 +642,8 @@ files = [ {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"}, {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, @@ -524,6 +653,8 @@ files = [ {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"}, {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, @@ -533,6 +664,8 @@ files = [ {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"}, {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, @@ -542,6 +675,8 @@ files = [ {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"}, {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, @@ -549,6 +684,8 @@ files = [ {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"}, {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, @@ -558,6 +695,8 @@ files = [ {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"}, {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, @@ -573,6 +712,7 @@ version = "0.4.8" description = "Pure-Python gRPC implementation for asyncio" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654"}, {file = "grpclib-0.4.8.tar.gz", hash = "sha256:d8823763780ef94fed8b2c562f7485cf0bbee15fc7d065a640673667f7719c9a"}, @@ -591,6 +731,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -602,6 +743,7 @@ version = "4.3.0" description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, @@ -611,12 +753,40 @@ files = [ hpack = ">=4.1,<5" hyperframe = ">=6.1,<7" +[[package]] +name = "hdwallet" +version = "3.6.1" +description = "Python-based library implementing a Hierarchical Deterministic (HD) Wallet generator for 200+ cryptocurrencies." +optional = false +python-versions = "<4,>=3.9" +groups = ["main"] +files = [ + {file = "hdwallet-3.6.1-py3-none-any.whl", hash = "sha256:787dc9041ddc496409dff94df4c46c93cc90bd8dd903748b9282269e9807e95b"}, + {file = "hdwallet-3.6.1.tar.gz", hash = "sha256:2a49192b0d1f1cded4e71daa27b17ffd37b6879ed5efa62143149a73cb4c0f9a"}, +] + +[package.dependencies] +base58 = ">=2.1.1,<3" +cbor2 = ">=5.6.1,<6" +coincurve = ">=20.0.0,<21" +crcmod = ">=1.7,<2" +ecdsa = ">=0.18.0,<1" +ed25519-blake2b = ">=1.4.1,<2" +pycryptodome = ">=3.20,<4" +pynacl = ">=1.5.0,<2" + +[package.extras] +cli = ["bip38 (>=1.4.1,<2)", "click (>=8.1.7,<9)", "click-aliases (>=1.0.5,<2)", "tabulate (>=0.9.0,<1)"] +docs = ["furo (==2024.8.6)", "sphinx (>=8.1.3,<9)", "sphinx-click (>=6.0.0,<7)"] +tests = ["coverage (>=7.6.4,<8)", "pytest (>=8.3.2,<9)", "tox (>=4.23.2,<5)"] + [[package]] name = "hpack" version = "4.1.0" description = "Pure-Python HPACK header encoding" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, @@ -628,6 +798,7 @@ version = "0.17.3" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, @@ -649,6 +820,7 @@ version = "0.24.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, @@ -661,7 +833,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -672,6 +844,7 @@ version = "6.1.0" description = "Pure-Python HTTP/2 framing" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, @@ -683,6 +856,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -697,17 +871,37 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] +tests = ["pytest (>=4.6)"] + [[package]] name = "multidict" version = "6.6.4" description = "multidict implementation" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, @@ -827,6 +1021,7 @@ version = "1.17.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, @@ -886,6 +1081,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -897,6 +1093,7 @@ version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, @@ -942,6 +1139,7 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -953,6 +1151,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -964,6 +1163,7 @@ version = "4.4.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"}, {file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"}, @@ -980,6 +1180,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -991,22 +1192,22 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "4.25.8" +version = "6.33.4" description = "" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0"}, - {file = "protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9"}, - {file = "protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f"}, - {file = "protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7"}, - {file = "protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0"}, - {file = "protobuf-4.25.8-cp38-cp38-win32.whl", hash = "sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af"}, - {file = "protobuf-4.25.8-cp38-cp38-win_amd64.whl", hash = "sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3"}, - {file = "protobuf-4.25.8-cp39-cp39-win32.whl", hash = "sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5"}, - {file = "protobuf-4.25.8-cp39-cp39-win_amd64.whl", hash = "sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24"}, - {file = "protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59"}, - {file = "protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd"}, + {file = "protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d"}, + {file = "protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc"}, + {file = "protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0"}, + {file = "protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e"}, + {file = "protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6"}, + {file = "protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9"}, + {file = "protobuf-6.33.4-cp39-cp39-win32.whl", hash = "sha256:955478a89559fa4568f5a81dce77260eabc5c686f9e8366219ebd30debf06aa6"}, + {file = "protobuf-6.33.4-cp39-cp39-win_amd64.whl", hash = "sha256:0f12ddbf96912690c3582f9dffb55530ef32015ad8e678cd494312bd78314c4f"}, + {file = "protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc"}, + {file = "protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91"}, ] [[package]] @@ -1015,6 +1216,7 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1026,6 +1228,7 @@ version = "3.23.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] files = [ {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, @@ -1076,6 +1279,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -1084,12 +1288,86 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pynacl" +version = "1.5.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.6" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and python_version >= \"3.14\"" +files = [ + {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, + {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, + {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, + {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, + {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, +] + +[package.dependencies] +cffi = ">=1.4.1" + +[package.extras] +docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] + +[[package]] +name = "pynacl" +version = "1.6.0" +description = "Python binding to the Networking and Cryptography (NaCl) library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +markers = "python_version < \"3.14\" or platform_python_implementation == \"PyPy\"" +files = [ + {file = "pynacl-1.6.0-cp314-cp314t-macosx_10_10_universal2.whl", hash = "sha256:f46386c24a65383a9081d68e9c2de909b1834ec74ff3013271f1bca9c2d233eb"}, + {file = "pynacl-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dea103a1afcbc333bc0e992e64233d360d393d1e63d0bc88554f572365664348"}, + {file = "pynacl-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:04f20784083014e265ad58c1b2dd562c3e35864b5394a14ab54f5d150ee9e53e"}, + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbcc4452a1eb10cd5217318c822fde4be279c9de8567f78bad24c773c21254f8"}, + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fed9fe1bec9e7ff9af31cd0abba179d0e984a2960c77e8e5292c7e9b7f7b5d"}, + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:10d755cf2a455d8c0f8c767a43d68f24d163b8fe93ccfaabfa7bafd26be58d73"}, + {file = "pynacl-1.6.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:536703b8f90e911294831a7fbcd0c062b837f3ccaa923d92a6254e11178aaf42"}, + {file = "pynacl-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b08eab48c9669d515a344fb0ef27e2cbde847721e34bba94a343baa0f33f1f4"}, + {file = "pynacl-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5789f016e08e5606803161ba24de01b5a345d24590a80323379fc4408832d290"}, + {file = "pynacl-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:4853c154dc16ea12f8f3ee4b7e763331876316cc3a9f06aeedf39bcdca8f9995"}, + {file = "pynacl-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:347dcddce0b4d83ed3f32fd00379c83c425abee5a9d2cd0a2c84871334eaff64"}, + {file = "pynacl-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2d6cd56ce4998cb66a6c112fda7b1fdce5266c9f05044fa72972613bef376d15"}, + {file = "pynacl-1.6.0-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:f4b3824920e206b4f52abd7de621ea7a44fd3cb5c8daceb7c3612345dfc54f2e"}, + {file = "pynacl-1.6.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16dd347cdc8ae0b0f6187a2608c0af1c8b7ecbbe6b4a06bff8253c192f696990"}, + {file = "pynacl-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16c60daceee88d04f8d41d0a4004a7ed8d9a5126b997efd2933e08e93a3bd850"}, + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25720bad35dfac34a2bcdd61d9e08d6bfc6041bebc7751d9c9f2446cf1e77d64"}, + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8bfaa0a28a1ab718bad6239979a5a57a8d1506d0caf2fba17e524dbb409441cf"}, + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ef214b90556bb46a485b7da8258e59204c244b1b5b576fb71848819b468c44a7"}, + {file = "pynacl-1.6.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:49c336dd80ea54780bcff6a03ee1a476be1612423010472e60af83452aa0f442"}, + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f3482abf0f9815e7246d461fab597aa179b7524628a4bc36f86a7dc418d2608d"}, + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:140373378e34a1f6977e573033d1dd1de88d2a5d90ec6958c9485b2fd9f3eb90"}, + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6b393bc5e5a0eb86bb85b533deb2d2c815666665f840a09e0aa3362bb6088736"}, + {file = "pynacl-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a25cfede801f01e54179b8ff9514bd7b5944da560b7040939732d1804d25419"}, + {file = "pynacl-1.6.0-cp38-abi3-win32.whl", hash = "sha256:dcdeb41c22ff3c66eef5e63049abf7639e0db4edee57ba70531fc1b6b133185d"}, + {file = "pynacl-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:cf831615cc16ba324240de79d925eacae8265b7691412ac6b24221db157f6bd1"}, + {file = "pynacl-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:84709cea8f888e618c21ed9a0efdb1a59cc63141c403db8bf56c469b71ad56f2"}, + {file = "pynacl-1.6.0.tar.gz", hash = "sha256:cb36deafe6e2bce3b286e5d1f3e1c246e0ccdb8808ddb4550bb2792f2df298f2"}, +] + +[package.dependencies] +cffi = {version = ">=1.4.1", markers = "platform_python_implementation != \"PyPy\" and python_version < \"3.14\""} + +[package.extras] +docs = ["sphinx (<7)", "sphinx_rtd_theme"] +tests = ["hypothesis (>=3.27.0)", "pytest (>=7.4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] + [[package]] name = "pytest" version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, @@ -1111,6 +1389,7 @@ version = "0.23.8" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, @@ -1123,12 +1402,24 @@ pytest = ">=7.0.0,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "python-bitcoinrpc" +version = "1.0" +description = "Enhanced version of python-jsonrpc for use with Bitcoin" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "python-bitcoinrpc-1.0.tar.gz", hash = "sha256:a6a6f35672635163bc491c25fe29520bdd063dedbeda3b37bf5be97aa038c6e7"}, +] + [[package]] name = "python-bitcointx" version = "1.1.5" description = "A library for handling Bitcoin transactions and associated data" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "python_bitcointx-1.1.5-py3-none-any.whl", hash = "sha256:7d961afac69dde865140e65af47c4ecc7b73a0697ced27244345539d8e94c555"}, ] @@ -1139,6 +1430,7 @@ version = "2.32.5" description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -1160,6 +1452,7 @@ version = "0.5.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, @@ -1187,6 +1480,7 @@ version = "3.0.4" description = "Python helper for Semantic Versioning (https://semver.org)" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, @@ -1198,6 +1492,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1209,6 +1504,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1220,6 +1516,7 @@ version = "2.0.43" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-2.0.43-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed"}, {file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227"}, @@ -1315,16 +1612,36 @@ version = "1.2.0" description = "String case converter." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, ] +[[package]] +name = "sympy" +version = "1.14.0" +description = "Computer algebra system (CAS) in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, + {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, +] + +[package.dependencies] +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] + [[package]] name = "typing-extensions" version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, @@ -1336,18 +1653,19 @@ version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [metadata] -lock-version = "2.0" -python-versions = ">=3.11" -content-hash = "1593cb0500fb3344f3f42691fc781836531a612ee93923decef2074340de52d6" +lock-version = "2.1" +python-versions = ">=3.11,<4" +content-hash = "ef79afa2a8a769944989310b88bbde79dd9145db8c0fee27914ad121969d82f7" diff --git a/packages/app_btc/pyproject.toml b/packages/app_btc/pyproject.toml index 018348f..e3f2506 100644 --- a/packages/app_btc/pyproject.toml +++ b/packages/app_btc/pyproject.toml @@ -7,13 +7,13 @@ readme = "README.md" packages = [{include = "src/app_btc"}] [tool.poetry.dependencies] -python = ">=3.11" +python = ">=3.11,<4" # Bitcoin libraries bitcoinlib = "^0.7.5" python-bitcointx = "^1.1.4" coincurve = "^20.0.0" base58 = "^2.1.1" -ecdsa = "^0.18.0" +ecdsa = "^0.19" bech32 = "^1.2.0" # HTTP client httpx = "^0.24.0" @@ -21,8 +21,9 @@ httpx = "^0.24.0" cryptography = "^41.0.0" # Protobuf betterproto = "^1.2.5" -protobuf = "^4.24.0" +protobuf = "^6.0.0" semver = "^3.0.4" +bitcoin-utils = "^0.7.3" [tool.poetry.group.dev.dependencies] @@ -42,7 +43,7 @@ line-length = 88 target-version = ['py311'] [tool.mypy] -python_version = "3.11" +python_version = "^3.11" warn_return_any = true warn_unused_configs = true ignore_missing_imports = true diff --git a/packages/app_btc/scripts/prebuild.sh b/packages/app_btc/scripts/prebuild.sh index cf49b33..2556fe6 100755 --- a/packages/app_btc/scripts/prebuild.sh +++ b/packages/app_btc/scripts/prebuild.sh @@ -8,21 +8,21 @@ cd "$(dirname "$0")/.." rm -rf ./src/app_btc/proto/generated/*.py || true rm -rf ./src/app_btc/proto/generated/btc || true +# Create output directory for generated files mkdir -p src/app_btc/proto/generated -# Use poetry run python3 to ensure we use the root environment with betterproto +# Use poetry run python3 to ensure we use the root environment with standard protoc PYTHON_CMD="poetry run python3" -protoc --python_betterproto_out=./src/app_btc/proto/generated \ +protoc --python_out=./src/app_btc/proto/generated \ --proto_path="../../submodules/common/proto" \ ../../submodules/common/proto/btc/core.proto \ ../../submodules/common/proto/btc/error.proto \ ../../submodules/common/proto/btc/get_public_key.proto \ ../../submodules/common/proto/btc/get_xpubs.proto \ - ../../submodules/common/proto/common.proto - -protoc --python_out=./src/app_btc/proto/generated \ - --proto_path="../../submodules/common/proto" \ - ../../submodules/common/proto/btc/sign_txn.proto || echo "Warning: sign_txn.proto generation failed - optional fields not supported" + ../../submodules/common/proto/btc/sign_txn.proto \ + ../../submodules/common/proto/common.proto \ + ../../submodules/common/proto/error.proto -$PYTHON_CMD ../../scripts/extract_types/__init__.py ./src/app_btc/proto/generated ./src/app_btc/proto/generated/types.py +# Fix imports in generated files +$PYTHON_CMD ../../scripts/fix_proto_imports.py ./src/app_btc/proto/generated btc diff --git a/packages/app_btc/src/app_btc/__init__.py b/packages/app_btc/src/app_btc/__init__.py index 44b2afa..27872b0 100644 --- a/packages/app_btc/src/app_btc/__init__.py +++ b/packages/app_btc/src/app_btc/__init__.py @@ -1,5 +1,4 @@ from .app import BtcApp -from .proto.generated.types import * from .operations.types import * from .utils import ( update_logger, diff --git a/packages/app_btc/src/app_btc/operations/getPublicKey/__init__.py b/packages/app_btc/src/app_btc/operations/getPublicKey/__init__.py index ba82fb4..0c71236 100644 --- a/packages/app_btc/src/app_btc/operations/getPublicKey/__init__.py +++ b/packages/app_btc/src/app_btc/operations/getPublicKey/__init__.py @@ -2,8 +2,8 @@ from util.utils import create_status_listener, create_logger_with_prefix from util.utils.assert_utils import assert_condition from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType -from ...proto.generated.btc import GetPublicKeyStatus -from ...proto.generated.common import SeedGenerationStatus +from ...proto.generated.btc.get_public_key_pb2 import GetPublicKeyStatus +from ...proto.generated.common_pb2 import SeedGenerationStatus from ...utils import ( assert_or_throw_invalid_result, OperationHelper, @@ -71,7 +71,7 @@ async def get_public_key( helper = OperationHelper( sdk=sdk, - query_key="getPublicKey", + query_key="get_public_key", result_key="get_public_key", on_status=on_status, ) diff --git a/packages/app_btc/src/app_btc/operations/getPublicKey/publicKeyToAddress/__init__.py b/packages/app_btc/src/app_btc/operations/getPublicKey/publicKeyToAddress/__init__.py index 2242144..ae42ee5 100644 --- a/packages/app_btc/src/app_btc/operations/getPublicKey/publicKeyToAddress/__init__.py +++ b/packages/app_btc/src/app_btc/operations/getPublicKey/publicKeyToAddress/__init__.py @@ -1,13 +1,14 @@ from typing import List -from coincurve import PublicKey -from ....utils import get_bitcoin_py_lib, get_network_from_path, get_purpose_type +from ....utils import get_network_from_path, get_purpose_type +from bitcoinutils.keys import PublicKey as BitcoinPublicKey, P2shAddress +from bitcoinutils.setup import setup def get_address_from_public_key(uncompressed_public_key: bytes, path: List[int]) -> str: """ - 1. Compress the uncompressed public key using secp256k1 - 2. Get the appropriate payment function based on the path - 3. Generate the address using the payment function + 1. Get the purpose type from the derivation path + 2. Create the bitcoin public key object from the uncompressed public key + 3. Get the address from the public key based on the purpose type 4. Assert that the address was generated successfully Args: @@ -20,28 +21,26 @@ def get_address_from_public_key(uncompressed_public_key: bytes, path: List[int]) Raises: AssertionError: If address could not be derived """ - if len(uncompressed_public_key) == 33: - compressed_public_key = uncompressed_public_key - elif len(uncompressed_public_key) == 65: - compressed_public_key = PublicKey(uncompressed_public_key).format( - compressed=True - ) - else: - raise ValueError( - f"Invalid public key length: {len(uncompressed_public_key)} bytes. Expected 33 (compressed) or 65 (uncompressed)." - ) - bitcoin_py_lib = get_bitcoin_py_lib() network_config = get_network_from_path(path) - network = "bitcoin" if network_config.pub_key_hash == 0 else "testnet" + network = "mainnet" if network_config.pub_key_hash == 0 else "testnet" + setup(network) purpose_type = get_purpose_type(path) - - if purpose_type == "segwit": - result = bitcoin_py_lib.payments.p2wpkh(compressed_public_key, network) + pubkey = BitcoinPublicKey(uncompressed_public_key.hex()) + + if purpose_type == "legacy": + address = pubkey.get_address() + elif purpose_type == "segwit": + address = pubkey.get_segwit_address() + elif purpose_type == "nested_segwit": + address = P2shAddress.from_script( + pubkey.get_segwit_address().to_script_pub_key() + ) + elif purpose_type == "taproot": + address = pubkey.get_taproot_address() else: - result = bitcoin_py_lib.payments.p2pkh(compressed_public_key, network) + raise ValueError(f"Unsupported purpose type: {purpose_type}") - address = result["address"] assert address, "Could not derive address" - return address + return address.to_string() diff --git a/packages/app_btc/src/app_btc/operations/getXpubs/__init__.py b/packages/app_btc/src/app_btc/operations/getXpubs/__init__.py index f1c5978..b5f1a9a 100644 --- a/packages/app_btc/src/app_btc/operations/getXpubs/__init__.py +++ b/packages/app_btc/src/app_btc/operations/getXpubs/__init__.py @@ -1,14 +1,15 @@ from core.types import ISDK from util.utils import create_status_listener, create_logger_with_prefix from util.utils.assert_utils import assert_condition -from ...proto.generated.btc import GetXpubsStatus, GetXpubsResultResponse -from ...proto.generated.common import SeedGenerationStatus +from ...proto.generated.btc.get_xpubs_pb2 import GetXpubsStatus, GetXpubsResultResponse +from ...proto.generated.common_pb2 import SeedGenerationStatus from ...utils import ( assert_or_throw_invalid_result, OperationHelper, logger as root_logger, configure_app_id, assert_derivation_path, + get_purpose_type, ) from .types import GetXpubsEvent, GetXpubsParams @@ -69,8 +70,8 @@ async def get_xpubs( helper = OperationHelper( sdk=sdk, - query_key="getXpubs", - result_key="getXpubs", + query_key="get_xpubs", + result_key="get_xpubs", on_status=on_status, ) @@ -89,4 +90,13 @@ async def get_xpubs( force_status_update(GetXpubsEvent.PIN_CARD) - return GetXpubsResultResponse(xpubs=result.result.xpubs) + return GetXpubsResultResponse( + xpubs=[ + ( + f"tr({xpub})" + if get_purpose_type(params.derivation_paths[i]["path"]) == "taproot" + else xpub + ) + for i, xpub in enumerate(result.result.xpubs) + ] + ) diff --git a/packages/app_btc/src/app_btc/operations/signTxn/__init__.py b/packages/app_btc/src/app_btc/operations/signTxn/__init__.py index fb6c60e..c5a2f4b 100644 --- a/packages/app_btc/src/app_btc/operations/signTxn/__init__.py +++ b/packages/app_btc/src/app_btc/operations/signTxn/__init__.py @@ -7,8 +7,8 @@ hex_to_uint8array, uint8array_to_hex, ) -from ...proto.generated.btc import SignTxnStatus -from ...proto.generated.common import SeedGenerationStatus +from ...proto.generated.btc.sign_txn_pb2 import SignTxnStatus +from ...proto.generated.common_pb2 import SeedGenerationStatus from ...utils import ( assert_or_throw_invalid_result, OperationHelper, @@ -18,6 +18,7 @@ AppFeatures, address_to_script_pub_key, create_signed_transaction, + get_purpose_type, ) from ...services.transaction import get_raw_txn_hash from .helpers import assert_sign_txn_params @@ -75,8 +76,8 @@ async def sign_txn( helper = OperationHelper( sdk=sdk, - query_key="signTxn", - result_key="signTxn", + query_key="sign_txn", + result_key="sign_txn", on_status=on_status, ) @@ -100,7 +101,12 @@ async def sign_txn( "locktime": params.txn.locktime or SIGN_TXN_DEFAULT_PARAMS["locktime"], "input_count": len(params.txn.inputs), "output_count": len(params.txn.outputs), - "sighash": params.txn.hash_type or SIGN_TXN_DEFAULT_PARAMS["hashtype"], + "sighash": params.txn.hash_type + or ( + 0 + if get_purpose_type(params.derivation_path) == "taproot" + else SIGN_TXN_DEFAULT_PARAMS["hashtype"] + ), } } ) @@ -112,12 +118,15 @@ async def sign_txn( for i, input_data in enumerate(params.txn.inputs): prev_txn_hash = bytes.fromhex(input_data.prev_txn_id)[::-1].hex() - prev_txn = input_data.prev_txn or await get_raw_txn_hash( - { - "hash": input_data.prev_txn_id, - "coin_type": get_coin_type_from_path(params.derivation_path), - } - ) + if input_data.prev_txn is not None: + prev_txn = input_data.prev_txn + else: + prev_txn = get_raw_txn_hash( + { + "hash": input_data.prev_txn_id, + "coinType": get_coin_type_from_path(params.derivation_path), + } + ) inputs[i].prev_txn = prev_txn await helper.send_query( @@ -130,7 +139,7 @@ async def sign_txn( input_data.address, params.derivation_path ) ), - "value": input_data.value, + "value": int(input_data.value), "sequence": input_data.sequence or SIGN_TXN_DEFAULT_PARAMS["input"]["sequence"], "change_index": input_data.change_index, @@ -156,7 +165,7 @@ async def sign_txn( output.address, params.derivation_path ) ), - "value": output.value, + "value": int(output.value), "is_change": output.is_change, "changes_index": output.address_index, } diff --git a/packages/app_btc/src/app_btc/operations/types.py b/packages/app_btc/src/app_btc/operations/types.py index 8135742..f2e6fe2 100644 --- a/packages/app_btc/src/app_btc/operations/types.py +++ b/packages/app_btc/src/app_btc/operations/types.py @@ -9,7 +9,7 @@ GetXpubsEventHandler, GetXpubsParams, ) -from ..proto.generated.btc import GetXpubsResultResponse +from ..proto.generated.btc.get_xpubs_pb2 import GetXpubsResultResponse from .signTxn.types import ( SignTxnEvent, SignTxnEventHandler, diff --git a/packages/app_btc/src/app_btc/services/transaction.py b/packages/app_btc/src/app_btc/services/transaction.py index 5d8f248..c78bdc4 100644 --- a/packages/app_btc/src/app_btc/services/transaction.py +++ b/packages/app_btc/src/app_btc/services/transaction.py @@ -4,7 +4,7 @@ base_url = "/v2/transaction" -async def get_raw_txn_hash(params: Dict[str, str]) -> str: +def get_raw_txn_hash(params: Dict[str, str]) -> str: """ Get raw transaction hash from the API. @@ -15,5 +15,5 @@ async def get_raw_txn_hash(params: Dict[str, str]) -> str: Raw transaction hex string """ - response = await http.post(f"{base_url}/hex", json=params) - return response.json()["data"]["data"] + response = http.post(f"{base_url}/hex", json=params) + return response.json()["data"] diff --git a/packages/app_btc/src/app_btc/utils/assert_utils.py b/packages/app_btc/src/app_btc/utils/assert_utils.py index a370f71..0ac9fef 100644 --- a/packages/app_btc/src/app_btc/utils/assert_utils.py +++ b/packages/app_btc/src/app_btc/utils/assert_utils.py @@ -1,7 +1,7 @@ from typing import TypeVar, Optional from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType from util.utils.assert_utils import assert_condition -from ..proto.generated.error import CommonError +from app_btc.proto.generated.error_pb2 import CommonError T = TypeVar("T") diff --git a/packages/app_btc/src/app_btc/utils/network.py b/packages/app_btc/src/app_btc/utils/network.py index f801462..acb521f 100644 --- a/packages/app_btc/src/app_btc/utils/network.py +++ b/packages/app_btc/src/app_btc/utils/network.py @@ -13,6 +13,8 @@ LEGACY_PURPOSE = HARDENED_BASE + 44 SEGWIT_PURPOSE = HARDENED_BASE + 84 +NESTED_SEGWIT_PURPOSE = HARDENED_BASE + 49 +TAPROOT_PURPOSE = HARDENED_BASE + 86 BITCOIN_COIN_INDEX = HARDENED_BASE + 0 TESTNET_COIN_INDEX = HARDENED_BASE + 1 @@ -98,11 +100,13 @@ def __init__( DASH_COIN_INDEX: dash, } -PurposeType = Literal["segwit", "legacy"] +PurposeType = Literal["segwit", "legacy", "nested_segwit", "taproot"] purpose_map: Dict[int, Optional[PurposeType]] = { SEGWIT_PURPOSE: "segwit", LEGACY_PURPOSE: "legacy", + NESTED_SEGWIT_PURPOSE: "nested_segwit", + TAPROOT_PURPOSE: "taproot", } coin_index_to_coin_type_map: Dict[int, Optional[str]] = { @@ -137,7 +141,7 @@ def get_coin_type_from_path(path: List[int]) -> str: supported_purpose_map: Dict[int, Optional[List[PurposeType]]] = { - BITCOIN_COIN_INDEX: ["legacy", "segwit"], + BITCOIN_COIN_INDEX: ["legacy", "segwit", "nested_segwit", "taproot"], LITECOIN_COIN_INDEX: ["legacy", "segwit"], DOGECOIN_COIN_INDEX: ["legacy"], DASH_COIN_INDEX: ["legacy"], diff --git a/packages/app_btc/src/app_btc/utils/operationHelper.py b/packages/app_btc/src/app_btc/utils/operationHelper.py index 979affd..e074e14 100644 --- a/packages/app_btc/src/app_btc/utils/operationHelper.py +++ b/packages/app_btc/src/app_btc/utils/operationHelper.py @@ -2,8 +2,8 @@ from core.sdk import ISDK from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType from util.utils.create_status_listener import OnStatus -from ..proto.generated.btc import Query, Result -from ..proto.generated.common import ChunkPayload +from ..proto.generated.btc.core_pb2 import Query, Result +from ..proto.generated.common_pb2 import ChunkPayload from ..utils.assert_utils import assert_or_throw_invalid_result, parse_common_error Q = TypeVar("Q") @@ -24,12 +24,9 @@ def decode_result(data: bytes) -> Result: DeviceAppError: If decoding fails """ try: - return Result.parse(data) - except TypeError: - try: - return Result().parse(data) - except Exception: - raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) + result = Result() + result.ParseFromString(data) + return result except Exception: raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) @@ -44,62 +41,8 @@ def encode_query(query: Dict[str, Any]) -> bytes: Returns: Encoded query bytes """ - if "get_public_key" in query: - from ..proto.generated.btc import ( - GetPublicKeyRequest, - GetPublicKeyIntiateRequest, - ) - - get_pub_key_data = query["get_public_key"] - if "initiate" in get_pub_key_data: - initiate_data = get_pub_key_data["initiate"] - initiate = GetPublicKeyIntiateRequest( - wallet_id=initiate_data["wallet_id"], - derivation_path=initiate_data["derivation_path"], - ) - request = GetPublicKeyRequest(initiate=initiate) - query_obj = Query(get_public_key=request) - else: - query_obj = Query() - elif "get_xpubs" in query: - from ..proto.generated.btc import ( - GetXpubsRequest, - GetXpubsIntiateRequest, - GetXpubDerivationPath, - ) - - get_xpubs_data = query["get_xpubs"] - if "initiate" in get_xpubs_data: - initiate_data = get_xpubs_data["initiate"] - derivation_paths = [ - GetXpubDerivationPath(path=dp["path"]) - for dp in initiate_data["derivation_paths"] - ] - initiate = GetXpubsIntiateRequest( - wallet_id=initiate_data["wallet_id"], derivation_paths=derivation_paths - ) - request = GetXpubsRequest(initiate=initiate) - query_obj = Query(get_xpubs=request) - else: - query_obj = Query() - elif "sign_txn" in query: - from ..proto.generated.btc import SignTxnRequest, SignTxnInitiateRequest - - sign_txn_data = query["sign_txn"] - if "initiate" in sign_txn_data: - initiate_data = sign_txn_data["initiate"] - initiate = SignTxnInitiateRequest( - wallet_id=initiate_data["wallet_id"], - derivation_path=initiate_data["derivation_path"], - ) - request = SignTxnRequest(initiate=initiate) - query_obj = Query(sign_txn=request) - else: - query_obj = Query() - else: - query_obj = Query() - - return bytes(query_obj) + query_obj = Query(**query) + return query_obj.SerializeToString() class OperationHelper(Generic[Q, R]): @@ -138,12 +81,6 @@ async def send_query(self, query: Dict[str, Any]) -> None: query: Query data """ op_key = self.query_key - if op_key == "getPublicKey": - op_key = "get_public_key" - elif op_key == "getXpubs": - op_key = "get_xpubs" - elif op_key == "signTxn": - op_key = "sign_txn" query_data = {op_key: query} encoded_query = encode_query(query_data) await self.sdk.send_query(encoded_query) @@ -162,12 +99,6 @@ async def wait_for_result(self) -> Any: result = decode_result(result_data) result_key = self.result_key - if result_key == "getPublicKey": - result_key = "get_public_key" - elif result_key == "getXpubs": - result_key = "get_xpubs" - elif result_key == "signTxn": - result_key = "sign_txn" if "." in result_key: parts = result_key.split(".") diff --git a/packages/app_btc/src/app_btc/utils/transaction.py b/packages/app_btc/src/app_btc/utils/transaction.py index 110eaea..0c02143 100644 --- a/packages/app_btc/src/app_btc/utils/transaction.py +++ b/packages/app_btc/src/app_btc/utils/transaction.py @@ -1,17 +1,24 @@ from typing import List, Dict, Any from ..utils.bitcoinlib import get_bitcoin_py_lib -from ..utils.network import get_network_from_path +from ..utils.network import get_network_from_path, get_purpose_type from util.utils.assert_utils import assert_condition from bitcoinlib.encoding import convert_der_sig +from bitcoinlib.keys import Address +from util.utils.crypto import hex_to_uint8array +from bitcoinutils.setup import setup +from bitcoinutils.transactions import ( + Transaction as UtilTransaction, + TxInput, + TxOutput, + TxWitnessInput, +) +from bitcoinutils.script import Script def address_to_script_pub_key(address: str, derivation_path: List[int]) -> str: - _ = get_bitcoin_py_lib() network = get_network_from_path(derivation_path) network_name = "bitcoin" if network.pub_key_hash == 0 else "testnet" - from bitcoinlib.keys import Address - addr_obj = Address.parse(address, network=network_name) if addr_obj.script_type == "p2wpkh": @@ -20,6 +27,10 @@ def address_to_script_pub_key(address: str, derivation_path: List[int]) -> str: script_pubkey = f"a914{addr_obj.hash_bytes.hex()}87" elif addr_obj.script_type == "p2pkh": script_pubkey = f"76a914{addr_obj.hash_bytes.hex()}88ac" + elif addr_obj.script_type == "p2sh": + script_pubkey = f"a914{addr_obj.hash_bytes.hex()}87" + elif addr_obj.script_type == "p2tr": + script_pubkey = f"5120{addr_obj.hash_bytes.hex()}" else: raise ValueError(f"Unsupported address type: {addr_obj.script_type}") @@ -30,21 +41,67 @@ def is_script_segwit(script: str) -> bool: return script.startswith("0014") +def is_script_nested_segwit(script: str) -> bool: + return script.startswith("a914") and script.endswith("87") and len(script) == 46 + + +def create_taproot_transaction(params: Dict[str, Any]) -> str: + inputs = params["inputs"] + outputs = params["outputs"] + signatures = params["signatures"] + derivation_path = params["derivation_path"] + + network = get_network_from_path(derivation_path) + network_name = "mainnet" if network.pub_key_hash == 0 else "testnet" + setup(network_name) + + txn_inputs = [] + for input_data in inputs: + txn_inputs.append( + TxInput( + input_data.prev_txn_id, + input_data.prev_index, + sequence=str(input_data.sequence or "ffffffff"), + ) + ) + + txn_outputs = [] + for output_data in outputs: + txn_outputs.append( + TxOutput( + int(output_data.value), + Script.from_raw( + address_to_script_pub_key(output_data.address, derivation_path) + ), + ) + ) + + txn = UtilTransaction(txn_inputs, txn_outputs, has_segwit=True) + + for signature in signatures: + txn.witnesses.append(TxWitnessInput([signature[:128]])) + + return txn.serialize() + + def create_signed_transaction(params: Dict[str, Any]) -> str: inputs = params["inputs"] outputs = params["outputs"] signatures = params["signatures"] derivation_path = params["derivation_path"] - _ = get_bitcoin_py_lib() + purpose_type = get_purpose_type(derivation_path) + if purpose_type == "taproot": + return create_taproot_transaction(params) + network = get_network_from_path(derivation_path) network_name = "bitcoin" if network.pub_key_hash == 0 else "testnet" - from bitcoinlib.transactions import Transaction + from bitcoinlib.transactions import Transaction, Input, Output, Key transaction = Transaction(network=network_name, version=2) - for input_data in inputs: + for i, input_data in enumerate(inputs): if hasattr(input_data, "address"): address = input_data.address prev_txn_id = input_data.prev_txn_id @@ -59,24 +116,50 @@ def create_signed_transaction(params: Dict[str, Any]) -> str: script = address_to_script_pub_key(address, derivation_path) is_segwit = is_script_segwit(script) - txn_input = { - "prev_txid": prev_txn_id, - "output_n": prev_index, - "value": int(value), - } + try: + signature = signatures[i] - if is_segwit: - txn_input["unlocking_script"] = "" - txn_input["witness_type"] = "segwit" - else: - if hasattr(input_data, "prev_txn"): - prev_txn = input_data.prev_txn - else: - prev_txn = input_data.get("prevTxn") - assert_condition(prev_txn, "prevTxn is required in input") - txn_input["unlocking_script"] = prev_txn + pubkey = signature[-66:] + k = Key(pubkey, compressed=True) - transaction.add_input(**txn_input) + der_length = int(signature[4:6], 16) * 2 + der_encoded = signature[2 : der_length + 6] + der_bytes = bytes.fromhex(der_encoded) + signature_hex = convert_der_sig(der_bytes, as_hex=True) + signature_bytes = bytes.fromhex(signature_hex) + except (ValueError, IndexError) as e: + k = None + signature_bytes = None + + # txn_input = { + # "prev_txid": prev_txn_id, + # "output_n": prev_index, + # "value": int(value), + # "address": address, + # } + + # if is_segwit: + # txn_input["unlocking_script"] = "" + # txn_input["witness_type"] = "segwit" + # txn_input["script_type"] = "p2wpkh" + # else: + # if hasattr(input_data, "prev_txn"): + # prev_txn = input_data.prev_txn + # else: + # prev_txn = input_data.get("prevTxn") + # assert_condition(prev_txn, "prevTxn is required in input") + # txn_input["unlocking_script"] = prev_txn + # txn_input["script_type"] = "p2pkh" + + transaction.add_input( + prev_txid=prev_txn_id, + output_n=prev_index, + value=int(value), + address=address, + keys=k.public_hex if k else None, + signatures=signature_bytes if signature_bytes else None, + witness_type="p2sh-segwit" if is_script_nested_segwit(script) else None, + ) for output in outputs: if hasattr(output, "address"): @@ -88,23 +171,23 @@ def create_signed_transaction(params: Dict[str, Any]) -> str: transaction.add_output(address=address, value=int(value)) - for i, signature in enumerate(signatures): - if not signature or signature == "": - continue - if len(signature) < 6: - continue - - try: - der_length = int(signature[4:6], 16) * 2 - der_encoded = signature[2 : der_length + 6] - _ = bytes.fromhex(signature[-66:]) - der_bytes = bytes.fromhex(der_encoded) - signature_hex = convert_der_sig(der_bytes, as_hex=True) - except (ValueError, IndexError) as e: - continue - - signature_bytes = bytes.fromhex(signature_hex) - _ = signature_bytes[:32] - _ = signature_bytes[32:64] - transaction.sign(signature_hex, i) + # for i, signature in enumerate(signatures): + # if not signature or signature == "": + # continue + # if len(signature) < 6: + # continue + + # try: + # der_length = int(signature[4:6], 16) * 2 + # der_encoded = signature[2 : der_length + 6] + # _ = bytes.fromhex(signature[-66:]) + # der_bytes = bytes.fromhex(der_encoded) + # signature_hex = convert_der_sig(der_bytes, as_hex=True) + # except (ValueError, IndexError) as e: + # continue + + # signature_bytes = bytes.fromhex(signature_hex) + # _ = signature_bytes[:32] + # _ = signature_bytes[32:64] + # transaction.sign(signature_hex, i) return transaction.raw_hex() diff --git a/packages/app_manager/.gitignore b/packages/app_manager/.gitignore new file mode 100644 index 0000000..213cfe2 --- /dev/null +++ b/packages/app_manager/.gitignore @@ -0,0 +1 @@ +src/app_manager/proto/generated \ No newline at end of file diff --git a/packages/app_manager/scripts/prebuild.sh b/packages/app_manager/scripts/prebuild.sh index 1c99aa7..ace3609 100755 --- a/packages/app_manager/scripts/prebuild.sh +++ b/packages/app_manager/scripts/prebuild.sh @@ -8,12 +8,13 @@ cd "$(dirname "$0")/.." rm -rf ./src/app_manager/proto/generated/*.py || true rm -rf ./src/app_manager/proto/generated/manager || true +# Create output directory for generated files mkdir -p src/app_manager/proto/generated -# Use poetry run python3 to ensure we use the root environment with betterproto +# Use poetry run python3 to ensure we use the root environment with standard protoc PYTHON_CMD="poetry run python3" -protoc --python_betterproto_out=./src/app_manager/proto/generated \ +protoc --python_out=./src/app_manager/proto/generated \ --proto_path="../../submodules/common/proto" \ ../../submodules/common/proto/manager/common.proto \ ../../submodules/common/proto/manager/core.proto \ @@ -23,6 +24,11 @@ protoc --python_betterproto_out=./src/app_manager/proto/generated \ ../../submodules/common/proto/manager/get_wallets.proto \ ../../submodules/common/proto/manager/train_card.proto \ ../../submodules/common/proto/manager/train_joystick.proto \ - ../../submodules/common/proto/manager/wallet_selector.proto + ../../submodules/common/proto/manager/wallet_selector.proto \ + ../../submodules/common/proto/manager/auth_device.proto \ + ../../submodules/common/proto/manager/auth_card.proto \ + ../../submodules/common/proto/error.proto \ + ../../submodules/common/proto/common.proto -$PYTHON_CMD ../../scripts/extract_types/__init__.py ./src/app_manager/proto/generated ./src/app_manager/proto/generated/types.py \ No newline at end of file +# Fix imports in generated files +$PYTHON_CMD ../../scripts/fix_proto_imports.py ./src/app_manager/proto/generated manager diff --git a/packages/app_manager/src/app_manager/__init__.py b/packages/app_manager/src/app_manager/__init__.py index bc7ff17..7fe8147 100644 --- a/packages/app_manager/src/app_manager/__init__.py +++ b/packages/app_manager/src/app_manager/__init__.py @@ -1,5 +1,4 @@ from .app import ManagerApp -from .proto.generated.types import * from .operations.types import * from .utils import update_logger diff --git a/packages/app_manager/src/app_manager/operations/auth_card/__init__.py b/packages/app_manager/src/app_manager/operations/auth_card/__init__.py index a459160..1a48f7b 100644 --- a/packages/app_manager/src/app_manager/operations/auth_card/__init__.py +++ b/packages/app_manager/src/app_manager/operations/auth_card/__init__.py @@ -11,7 +11,7 @@ ) from app_manager.constants.appId import APP_VERSION -from app_manager.proto.generated.types import AuthCardStatus +from app_manager.proto.generated.manager.auth_card_pb2 import AuthCardStatus from ...services import card_auth as card_auth_service from ...utils import ( assert_or_throw_invalid_result, diff --git a/packages/app_manager/src/app_manager/operations/auth_card/types.py b/packages/app_manager/src/app_manager/operations/auth_card/types.py index 36ee9cc..4354936 100644 --- a/packages/app_manager/src/app_manager/operations/auth_card/types.py +++ b/packages/app_manager/src/app_manager/operations/auth_card/types.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from typing import Callable, Optional -from app_manager.proto.generated.types import AuthCardStatus +from app_manager.proto.generated.manager.auth_card_pb2 import AuthCardStatus AuthCardEventHandler = Callable[[AuthCardStatus], None] diff --git a/packages/app_manager/src/app_manager/operations/getDeviceInfo/__init__.py b/packages/app_manager/src/app_manager/operations/getDeviceInfo/__init__.py index efbde7f..44fc37c 100644 --- a/packages/app_manager/src/app_manager/operations/getDeviceInfo/__init__.py +++ b/packages/app_manager/src/app_manager/operations/getDeviceInfo/__init__.py @@ -1,7 +1,7 @@ from core.types import ISDK from util.utils import create_logger_with_prefix from ...constants.appId import APP_VERSION -from ...proto.generated.manager import GetDeviceInfoResultResponse +from ...proto.generated.manager.get_device_info_pb2 import GetDeviceInfoResultResponse from ...utils import assert_or_throw_invalid_result, OperationHelper from ...utils import logger as rootlogger @@ -11,13 +11,10 @@ async def get_device_info(sdk: ISDK) -> GetDeviceInfoResultResponse: logger.info("Started") await sdk.check_app_compatibility(APP_VERSION) - - helper = OperationHelper(sdk, "getDeviceInfo", "getDeviceInfo") - + helper = OperationHelper(sdk, "get_device_info", "get_device_info") await helper.send_query({"initiate": {}}) result = await helper.wait_for_result() logger.verbose("GetDeviceInfoResponse", {"result": result}) assert_or_throw_invalid_result(result.result) - logger.info("Completed") return result.result diff --git a/packages/app_manager/src/app_manager/operations/getLogs/__init__.py b/packages/app_manager/src/app_manager/operations/getLogs/__init__.py index 0f18755..93f81bd 100644 --- a/packages/app_manager/src/app_manager/operations/getLogs/__init__.py +++ b/packages/app_manager/src/app_manager/operations/getLogs/__init__.py @@ -2,7 +2,7 @@ from core.types import ISDK from util.utils import create_logger_with_prefix, create_status_listener from ...constants.appId import APP_VERSION -from ...proto.generated.manager import GetLogsStatus, GetLogsErrorResponse +from ...proto.generated.manager.get_logs_pb2 import GetLogsStatus, GetLogsErrorResponse from ...utils import assert_or_throw_invalid_result, OperationHelper from ...utils import logger as rootlogger from .types import GetLogsError, GetLogsErrorType, GetLogsEventHandler @@ -39,17 +39,19 @@ async def get_logs( on_event: Optional[GetLogsEventHandler] = None, ) -> str: logger.info("Started") - helper = OperationHelper(sdk, "getLogs", "getLogs") + helper = OperationHelper(sdk, "get_logs", "get_logs") await sdk.check_app_compatibility(APP_VERSION) - on_status, force_status_update = create_status_listener( + status_listener = create_status_listener( { "enums": GetLogsStatus, "onEvent": on_event, "logger": logger, } ) + on_status = status_listener["onStatus"] + force_status_update = status_listener["forceStatusUpdate"] # ASCII decoder for log data def decode_ascii(data: bytes) -> str: @@ -68,12 +70,12 @@ def decode_ascii(data: bytes) -> str: force_status_update(GetLogsStatus.GET_LOGS_STATUS_USER_CONFIRMED) is_confirmed = True - has_more = result.hasMore + has_more = result.has_more all_logs.append(decode_ascii(result.data)) if has_more: - await helper.send_query({"fetchNext": {}}) + await helper.send_query({"fetch_next": {}}) else: break diff --git a/packages/app_manager/src/app_manager/operations/getLogs/types.py b/packages/app_manager/src/app_manager/operations/getLogs/types.py index f971b04..dff8238 100644 --- a/packages/app_manager/src/app_manager/operations/getLogs/types.py +++ b/packages/app_manager/src/app_manager/operations/getLogs/types.py @@ -1,5 +1,5 @@ from typing import Callable -from app_manager.proto.generated.types import GetLogsStatus +from app_manager.proto.generated.manager.get_logs_pb2 import GetLogsStatus from .error import GetLogsError, GetLogsErrorType # Re-export error types diff --git a/packages/app_manager/src/app_manager/operations/getWallets/__init__.py b/packages/app_manager/src/app_manager/operations/getWallets/__init__.py index 6ba0aea..e906d2a 100644 --- a/packages/app_manager/src/app_manager/operations/getWallets/__init__.py +++ b/packages/app_manager/src/app_manager/operations/getWallets/__init__.py @@ -1,7 +1,7 @@ from core.types import ISDK from util.utils import create_logger_with_prefix from app_manager.constants.appId import APP_VERSION -from app_manager.proto.generated.manager import GetWalletsResultResponse +from app_manager.proto.generated.manager.get_wallets_pb2 import GetWalletsResultResponse from ...utils import assert_or_throw_invalid_result, OperationHelper from ...utils import logger as rootlogger @@ -13,11 +13,11 @@ async def get_wallets(sdk: ISDK) -> GetWalletsResultResponse: await sdk.check_app_compatibility(APP_VERSION) - helper = OperationHelper(sdk, "getWallets", "getWallets") + helper = OperationHelper(sdk, "get_wallets", "get_wallets") await helper.send_query({"initiate": {}}) result = await helper.wait_for_result() - logger.verbose("GetWalletsResponse", result) + logger.verbose("GetWalletsResponse", {"result": result}) assert_or_throw_invalid_result(result.result) logger.info("Completed") diff --git a/packages/app_manager/src/app_manager/operations/selectWallet/__init__.py b/packages/app_manager/src/app_manager/operations/selectWallet/__init__.py index 356e719..728bf95 100644 --- a/packages/app_manager/src/app_manager/operations/selectWallet/__init__.py +++ b/packages/app_manager/src/app_manager/operations/selectWallet/__init__.py @@ -1,7 +1,9 @@ from core.types import ISDK from util.utils import create_logger_with_prefix from app_manager.constants.appId import APP_VERSION -from app_manager.proto.generated.manager import SelectWalletResultResponse +from app_manager.proto.generated.manager.wallet_selector_pb2 import ( + SelectWalletResultResponse, +) from ...utils import assert_or_throw_invalid_result, OperationHelper from ...utils import logger as rootlogger @@ -13,11 +15,11 @@ async def select_wallet(sdk: ISDK) -> SelectWalletResultResponse: await sdk.check_app_compatibility(APP_VERSION) - helper = OperationHelper(sdk, "selectWallet", "selectWallet") + helper = OperationHelper(sdk, "select_wallet", "select_wallet") await helper.send_query({"initiate": {}}) result = await helper.wait_for_result() - logger.verbose("SelectWalletResponse", result) + logger.verbose("SelectWalletResponse", {"result": result}) assert_or_throw_invalid_result(result.result) logger.info("Completed") diff --git a/packages/app_manager/src/app_manager/operations/trainCard/__init__.py b/packages/app_manager/src/app_manager/operations/trainCard/__init__.py index 3f00c7a..ee54dc0 100644 --- a/packages/app_manager/src/app_manager/operations/trainCard/__init__.py +++ b/packages/app_manager/src/app_manager/operations/trainCard/__init__.py @@ -1,7 +1,10 @@ from core.types import ISDK from util.utils import create_logger_with_prefix, create_status_listener from app_manager.constants.appId import APP_VERSION -from app_manager.proto.generated.manager import TrainCardResult, TrainCardStatus +from app_manager.proto.generated.manager.train_card_pb2 import ( + TrainCardResult, + TrainCardStatus, +) from ...utils import assert_or_throw_invalid_result, OperationHelper from ...utils import logger as rootlogger from .types import ITrainCardParams, TrainCardEventHandler @@ -20,7 +23,7 @@ async def train_card( await sdk.check_app_compatibility(APP_VERSION) - helper = OperationHelper(sdk, "trainCard", "trainCard") + helper = OperationHelper(sdk, "train_card", "train_card") on_status, force_status_update = create_status_listener( { diff --git a/packages/app_manager/src/app_manager/operations/trainCard/types.py b/packages/app_manager/src/app_manager/operations/trainCard/types.py index 1ac6d70..c399c48 100644 --- a/packages/app_manager/src/app_manager/operations/trainCard/types.py +++ b/packages/app_manager/src/app_manager/operations/trainCard/types.py @@ -1,5 +1,8 @@ from typing import Callable, Optional, Protocol -from app_manager.proto.generated.manager import TrainCardResult, TrainCardStatus +from app_manager.proto.generated.manager.train_card_pb2 import ( + TrainCardResult, + TrainCardStatus, +) # Re-export types __all__ = ["TrainCardEventHandler", "ITrainCardParams"] diff --git a/packages/app_manager/src/app_manager/operations/trainJoystick/__init__.py b/packages/app_manager/src/app_manager/operations/trainJoystick/__init__.py index 91e76f6..a293e50 100644 --- a/packages/app_manager/src/app_manager/operations/trainJoystick/__init__.py +++ b/packages/app_manager/src/app_manager/operations/trainJoystick/__init__.py @@ -2,7 +2,7 @@ from core.types import ISDK from util.utils import create_logger_with_prefix, create_status_listener from app_manager.constants.appId import APP_VERSION -from app_manager.proto.generated.types import TrainJoystickStatus +from app_manager.proto.generated.manager.train_joystick_pb2 import TrainJoystickStatus from ...utils import assert_or_throw_invalid_result, OperationHelper from ...utils import logger as rootlogger from .types import TrainJoystickEventHandler diff --git a/packages/app_manager/src/app_manager/operations/trainJoystick/types.py b/packages/app_manager/src/app_manager/operations/trainJoystick/types.py index 009461f..299594e 100644 --- a/packages/app_manager/src/app_manager/operations/trainJoystick/types.py +++ b/packages/app_manager/src/app_manager/operations/trainJoystick/types.py @@ -1,5 +1,5 @@ from typing import Callable, Optional -from app_manager.proto.generated.types import TrainJoystickStatus +from app_manager.proto.generated.manager.train_joystick_pb2 import TrainJoystickStatus # Re-export types __all__ = ["TrainJoystickEventHandler"] diff --git a/packages/app_manager/src/app_manager/operations/updateFirmware/__init__.py b/packages/app_manager/src/app_manager/operations/updateFirmware/__init__.py index 2c705dc..b493af3 100644 --- a/packages/app_manager/src/app_manager/operations/updateFirmware/__init__.py +++ b/packages/app_manager/src/app_manager/operations/updateFirmware/__init__.py @@ -7,7 +7,7 @@ uint8array_to_hex, ) from app_manager.constants.appId import APP_VERSION -from app_manager.proto.generated.manager import ( +from app_manager.proto.generated.manager.firmware_update_pb2 import ( FirmwareUpdateErrorResponse, FirmwareUpdateError, ) diff --git a/packages/app_manager/src/app_manager/operations/updateFirmware/helpers/legacy.py b/packages/app_manager/src/app_manager/operations/updateFirmware/helpers/legacy.py index f5ffcf3..02f0c13 100644 --- a/packages/app_manager/src/app_manager/operations/updateFirmware/helpers/legacy.py +++ b/packages/app_manager/src/app_manager/operations/updateFirmware/helpers/legacy.py @@ -4,7 +4,7 @@ DeviceCompatibilityErrorType, ) from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType -from core.encoders.proto.generated.common import Version +from app_manager.proto.generated.common_pb2 import Version def create_version_hex(version: Version) -> str: diff --git a/packages/app_manager/src/app_manager/operations/updateFirmware/types.py b/packages/app_manager/src/app_manager/operations/updateFirmware/types.py index 2eb1bd7..7f9d0f7 100644 --- a/packages/app_manager/src/app_manager/operations/updateFirmware/types.py +++ b/packages/app_manager/src/app_manager/operations/updateFirmware/types.py @@ -1,6 +1,6 @@ from typing import Callable, Optional, Protocol from interfaces import IDevice, IDeviceConnection -from core.encoders.proto.generated.common import Version +from app_manager.proto.generated.common_pb2 import Version from app_manager.proto.types import UpdateFirmwareStatus # Re-export types diff --git a/packages/app_manager/src/app_manager/proto/types.py b/packages/app_manager/src/app_manager/proto/types.py new file mode 100644 index 0000000..5f551de --- /dev/null +++ b/packages/app_manager/src/app_manager/proto/types.py @@ -0,0 +1,9 @@ +# UpdateFirmwareStatus enum - used for status tracking during firmware update +from enum import Enum + +class UpdateFirmwareStatus(Enum): + UPDATE_FIRMWARE_STATUS_INIT = 0 + UPDATE_FIRMWARE_STATUS_USER_CONFIRMED = 1 + UNRECOGNIZED = -1 + +__all__ = ["UpdateFirmwareStatus"] diff --git a/packages/app_manager/src/app_manager/utils/assert_utils.py b/packages/app_manager/src/app_manager/utils/assert_utils.py index 02960cf..b4fdb27 100644 --- a/packages/app_manager/src/app_manager/utils/assert_utils.py +++ b/packages/app_manager/src/app_manager/utils/assert_utils.py @@ -1,7 +1,7 @@ from typing import TypeVar, Optional from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType from util.utils.assert_utils import assert_condition -from app_manager.proto.generated.error import CommonError +from app_manager.proto.generated.error_pb2 import CommonError T = TypeVar("T") @@ -49,9 +49,8 @@ def parse_common_error(error: Optional[CommonError]) -> None: ("user_rejection", DeviceAppErrorType.USER_REJECTION), ("corrupt_data", DeviceAppErrorType.CORRUPT_DATA), ] - for field_name, error_type in error_fields: - if hasattr(error, field_name): + if getattr(error, field_name) != 0: error_value = getattr(error, field_name) if error_value is not None: raise DeviceAppError(error_type, error_value) diff --git a/packages/app_manager/src/app_manager/utils/operations_helper.py b/packages/app_manager/src/app_manager/utils/operations_helper.py index c8f6a67..d086687 100644 --- a/packages/app_manager/src/app_manager/utils/operations_helper.py +++ b/packages/app_manager/src/app_manager/utils/operations_helper.py @@ -1,8 +1,8 @@ from typing import TypeVar, Generic, Callable, Optional, Any, Dict from core.types import ISDK -from core.encoders.proto.generated.core import Status +from core.encoders.proto.generated.core_pb2 import Status from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType -from app_manager.proto.generated.manager import Query, Result +from app_manager.proto.generated.manager.core_pb2 import Query, Result from ..utils.assert_utils import assert_or_throw_invalid_result, parse_common_error Q = TypeVar("Q") @@ -11,7 +11,7 @@ def decode_result(data: bytes) -> Result: """ - Decode result data from bytes. + Decode result data from bytes using standard protobuf. Args: data: The bytes to decode @@ -23,7 +23,9 @@ def decode_result(data: bytes) -> Result: DeviceAppError: If decoding fails """ try: - return Result().parse(data) + result = Result() + result.ParseFromString(data) + return result except Exception as error: raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) from error @@ -36,10 +38,10 @@ def encode_query(query_data: Dict[str, Any]) -> bytes: query_data: Dictionary with query field and value Returns: - Encoded query as bytes (equivalent to Query.encode().finish()) + Encoded query as bytes """ query = Query(**query_data) - return bytes(query) + return query.SerializeToString() class OperationHelper(Generic[Q, R]): @@ -89,8 +91,7 @@ async def wait_for_result( params = {"on_status": on_status} if on_status else None result_data = await self.sdk.wait_for_result(params=params) result = decode_result(result_data) - - if hasattr(result, "common_error") and result.common_error: + if result.common_error: parse_common_error(result.common_error) result_value = getattr(result, self.result_key, None) diff --git a/packages/core/.gitignore b/packages/core/.gitignore new file mode 100644 index 0000000..86f0865 --- /dev/null +++ b/packages/core/.gitignore @@ -0,0 +1 @@ +src/core/encoders/proto/generated \ No newline at end of file diff --git a/packages/core/scripts/prebuild.sh b/packages/core/scripts/prebuild.sh index 19b0aba..98409f4 100755 --- a/packages/core/scripts/prebuild.sh +++ b/packages/core/scripts/prebuild.sh @@ -1,19 +1,21 @@ #!/bin/bash -# Python SDK proto compiler using betterproto (equivalent to TypeScript version) +# Python SDK proto compiler using standard protoc set -e # Ensure we're in the correct directory cd "$(dirname "$0")/.." -# Create output directory +rm -rf ./src/core/encoders/proto/generated/*.py || true + +# Create output directory for generated files mkdir -p src/core/encoders/proto/generated -# Use poetry run python3 to ensure we use the root environment with betterproto +# Use poetry run python3 to ensure we use the root environment with standard protoc PYTHON_CMD="poetry run python3" -# Step 1: Compile .proto files using betterproto (equivalent to protoc + ts-proto) -protoc --python_betterproto_out=./src/core/encoders/proto/generated \ +# Compile .proto files using standard protoc +protoc --python_out=./src/core/encoders/proto/generated \ --proto_path="../../submodules/common/proto" \ ../../submodules/common/proto/common.proto \ ../../submodules/common/proto/core.proto \ @@ -21,5 +23,5 @@ protoc --python_betterproto_out=./src/core/encoders/proto/generated \ ../../submodules/common/proto/session.proto \ ../../submodules/common/proto/version.proto -# Step 2: Extract and consolidate types (equivalent to extractTypes/index.js) -$PYTHON_CMD ../../scripts/extract_types/__init__.py ./src/core/encoders/proto/generated ./src/core/encoders/proto/generated/types.py +# Fix imports in generated files +$PYTHON_CMD ../../scripts/fix_proto_imports.py ./src/core/encoders/proto/generated core diff --git a/packages/core/src/core/commands/closeSession.py b/packages/core/src/core/commands/closeSession.py index 9d48fcb..7a7277f 100644 --- a/packages/core/src/core/commands/closeSession.py +++ b/packages/core/src/core/commands/closeSession.py @@ -1,7 +1,7 @@ from typing import Optional, Callable, Dict, Any, Awaitable from interfaces import IDeviceConnection -from ..encoders.proto.generated.core import ( - Msg, +from ..encoders.proto.generated.core_pb2 import Msg +from ..encoders.proto.generated.session_pb2 import ( SessionCloseCmd, SessionCloseRequest, SessionCloseClearRequest, @@ -44,7 +44,7 @@ async def send_session_command(params: CloseSessionParams): max_tries = params.options.get("maxTries") timeout = params.options.get("timeout") msg = build_session_close_msg() - msg_data = uint8array_to_hex(bytes(msg)) + msg_data = uint8array_to_hex(msg.SerializeToString()) await send_command( connection=params.connection, proto_data=msg_data, @@ -68,7 +68,8 @@ async def wait_for_session_result(params: CloseSessionParams) -> SessionCloseRes options=params.options, ) try: - msg = Msg.parse(result) + msg = Msg() + msg.ParseFromString(result) except Exception: raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) response = ( diff --git a/packages/core/src/core/commands/getAppVersion.py b/packages/core/src/core/commands/getAppVersion.py index b3ca63c..26b0c27 100644 --- a/packages/core/src/core/commands/getAppVersion.py +++ b/packages/core/src/core/commands/getAppVersion.py @@ -1,7 +1,7 @@ from typing import Optional, Callable, Dict, Any from interfaces import IDeviceConnection -from ..encoders.proto.generated.core import ( - Msg, +from ..encoders.proto.generated.core_pb2 import Msg +from ..encoders.proto.generated.version_pb2 import ( AppVersionCmd, AppVersionRequest, AppVersionIntiateRequest, @@ -40,7 +40,7 @@ async def get_app_versions(params: GetAppVersionsParams): request=AppVersionRequest(initiate=AppVersionIntiateRequest()) ) ) - msg_data = uint8array_to_hex(bytes(msg)) + msg_data = uint8array_to_hex(msg.SerializeToString()) await send_command( connection=params.connection, proto_data=msg_data, @@ -60,12 +60,8 @@ async def get_app_versions(params: GetAppVersionsParams): options=params.options, ) try: - msg = Msg.parse(result) - except TypeError: - try: - msg = Msg().parse(result) - except Exception: - raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) + msg = Msg() + msg.ParseFromString(result) except Exception: raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) response = ( diff --git a/packages/core/src/core/commands/startSession.py b/packages/core/src/core/commands/startSession.py index fb794b1..fb7f80b 100644 --- a/packages/core/src/core/commands/startSession.py +++ b/packages/core/src/core/commands/startSession.py @@ -1,7 +1,7 @@ from typing import Optional, Callable, Dict, Any, Awaitable from interfaces import IDeviceConnection -from ..encoders.proto.generated.core import ( - Msg, +from ..encoders.proto.generated.core_pb2 import Msg +from ..encoders.proto.generated.session_pb2 import ( SessionStartCmd, SessionStartRequest, SessionStartInitiateRequest, @@ -63,7 +63,7 @@ async def send_session_command(params: StartSessionParams, data: dict): max_tries = params.options.get("maxTries") timeout = params.options.get("timeout") msg = build_session_start_msg(data) - msg_data = uint8array_to_hex(bytes(msg)) + msg_data = uint8array_to_hex(msg.SerializeToString()) await send_command( connection=params.connection, proto_data=msg_data, @@ -87,7 +87,8 @@ async def wait_for_session_result(params: StartSessionParams) -> SessionStartRes options=params.options, ) try: - msg = Msg.parse(result) + msg = Msg() + msg.ParseFromString(result) except Exception: raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) response = ( diff --git a/packages/core/src/core/encoders/types.py b/packages/core/src/core/encoders/types.py index 0efe911..73a4c70 100644 --- a/packages/core/src/core/encoders/types.py +++ b/packages/core/src/core/encoders/types.py @@ -1,5 +1,5 @@ # Export all types from proto/types -from .proto.types import ( +from .proto.generated.core_pb2 import ( Status, DeviceIdleState, DeviceWaitingOn, diff --git a/packages/core/src/core/operations/helpers/getcommandoutput.py b/packages/core/src/core/operations/helpers/getcommandoutput.py index d83d680..ec09d23 100644 --- a/packages/core/src/core/operations/helpers/getcommandoutput.py +++ b/packages/core/src/core/operations/helpers/getcommandoutput.py @@ -44,7 +44,7 @@ async def get_command_output( raw_data=int_to_uint_byte(current_packet, 16), version=version, sequence_number=sequence_number, - packet_type=usable_config.commands.PACKET_TYPE.CMD_OUTPUT, + packet_type=usable_config.commands.PACKET_TYPE.CMD_OUTPUT_REQ, ) if len(packets_list) > 1: diff --git a/packages/core/src/core/operations/proto/__init__.py b/packages/core/src/core/operations/proto/__init__.py new file mode 100644 index 0000000..d14cdfb --- /dev/null +++ b/packages/core/src/core/operations/proto/__init__.py @@ -0,0 +1,15 @@ +from .getstatus import get_status +from .getresult import get_result +from .sendquery import send_query +from .waitForResult import wait_for_result +from .sendabort import send_abort +from .waitforidle import wait_for_idle + +__all__ = [ + "get_status", + "get_result", + "send_query", + "wait_for_result", + "send_abort", + "wait_for_idle", +] diff --git a/packages/core/src/core/operations/proto/getresult.py b/packages/core/src/core/operations/proto/getresult.py new file mode 100644 index 0000000..8264292 --- /dev/null +++ b/packages/core/src/core/operations/proto/getresult.py @@ -0,0 +1,87 @@ +from typing import Optional, Union, Dict +from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType +from interfaces import IDeviceConnection +from util.utils.assert_utils import assert_condition +from util.utils.crypto import hex_to_uint8array +from ...utils.packetversion import PacketVersion +from ...operations.helpers.getcommandoutput import get_command_output +from ...encoders.proto.generated import core_pb2 +from ...encoders.proto.generated.core_pb2 import Status, Msg, ErrorType + + +async def get_result( + connection: IDeviceConnection, + version: PacketVersion, + sequence_number: int, + applet_id: int, + max_tries: int = 5, + timeout: Optional[int] = None, + allow_core_data: Optional[bool] = None, +) -> Dict[str, Union[bool, Union[Status, bytes]]]: + assert_condition(applet_id, 'Invalid appletId') + + command_output = await get_command_output( + connection=connection, + version=version, + max_tries=max_tries, + sequence_number=sequence_number, + timeout=timeout + ) + + is_status = command_output["is_status"] + protobuf_data = command_output["protobuf_data"] + raw_data = command_output["raw_data"] + + output: Union[bytes, Status] + + if is_status: + status = Status() + status.ParseFromString(hex_to_uint8array(protobuf_data)) + if status.current_cmd_seq != sequence_number: + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + output = status + else: + msg = Msg() + msg.ParseFromString(hex_to_uint8array(protobuf_data)) + + # Determine which oneof is set and route accordingly + active_field = None + try: + active_field = msg.WhichOneof("type") + except Exception: + active_field = None + if not active_field: + try: + applet = getattr(msg, "cmd", None) + if applet is not None and getattr(applet, "applet_id", 0): + active_field = "cmd" + except Exception: + active_field = None + + # Error handling only if error oneof is active and not NO_ERROR + if active_field == "error" and msg.error and msg.error.type != ErrorType.NO_ERROR: + error_map = { + ErrorType.NO_ERROR: DeviceAppErrorType.UNKNOWN_ERROR, + ErrorType.UNKNOWN_APP: DeviceAppErrorType.UNKNOWN_APP, + ErrorType.INVALID_MSG: DeviceAppErrorType.INVALID_MSG, + ErrorType.APP_NOT_ACTIVE: DeviceAppErrorType.APP_NOT_ACTIVE, + ErrorType.APP_TIMEOUT_OCCURRED: DeviceAppErrorType.APP_TIMEOUT, + ErrorType.DEVICE_SESSION_INVALID: DeviceAppErrorType.DEVICE_SESSION_INVALID, + } + raise DeviceAppError(error_map[msg.error.type]) + + # If command oneof is active, validate applet id and return raw_data; otherwise return protobuf_data + if active_field == "cmd": + if not msg.cmd: + raise DeviceAppError(DeviceAppErrorType.INVALID_MSG_FROM_DEVICE) + if msg.cmd.applet_id != applet_id: + raise DeviceAppError(DeviceAppErrorType.INVALID_APP_ID_FROM_DEVICE) + # If raw_data is empty, treat it as a protobuf-only response + if raw_data: + output = hex_to_uint8array(raw_data) + else: + output = hex_to_uint8array(protobuf_data) + else: + output = hex_to_uint8array(protobuf_data) + + return {"is_status": is_status, "result": output} \ No newline at end of file diff --git a/packages/core/src/core/operations/proto/getstatus.py b/packages/core/src/core/operations/proto/getstatus.py new file mode 100644 index 0000000..611bd24 --- /dev/null +++ b/packages/core/src/core/operations/proto/getstatus.py @@ -0,0 +1,37 @@ +from typing import Optional +from interfaces import IDeviceConnection +from util.utils.crypto import hex_to_uint8array +from ...utils.logger import logger +from ...utils.packetversion import PacketVersion +from ...operations.helpers.getstatus import get_status as get_status_helper +from ...encoders.proto.generated.core_pb2 import Status + + +async def get_status( + connection: IDeviceConnection, + version: PacketVersion, + max_tries: int = 5, + timeout: Optional[int] = None, + dont_log: bool = False, +) -> Status: + result = await get_status_helper( + connection=connection, + version=version, + max_tries=max_tries, + timeout=timeout, + ) + + protobuf_data = result["protobuf_data"] + # Parse using standard protobuf + status = Status() + status.ParseFromString(hex_to_uint8array(protobuf_data)) + + if not dont_log: + try: + # Standard protobuf doesn't have to_dict(), use str representation + meta = {'status': str(status)} + except Exception: + meta = {'status': str(status)} + logger.debug('Received status', meta) + + return status \ No newline at end of file diff --git a/packages/core/src/core/operations/proto/sendabort.py b/packages/core/src/core/operations/proto/sendabort.py new file mode 100644 index 0000000..67413ae --- /dev/null +++ b/packages/core/src/core/operations/proto/sendabort.py @@ -0,0 +1,94 @@ +from typing import Optional +from interfaces import IDeviceConnection +from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType +from interfaces.errors.compatibility_error import DeviceCompatibilityError, DeviceCompatibilityErrorType +from util.utils.crypto import hex_to_uint8array +from ...utils.packetversion import PacketVersion, PacketVersionMap +from core.config import v3 as config +from ...encoders.packet.packet import decode_payload_data, encode_packet +from ...encoders.proto.generated.core_pb2 import Status +from ...operations.helpers.writecommand import write_command +from ...operations.helpers.can_retry import can_retry +from ...utils.logger import logger +from .waitforidle import wait_for_idle + + +async def send_abort( + connection: IDeviceConnection, + version: PacketVersion, + sequence_number: int, + max_tries: int = 2, + timeout: Optional[int] = None, +) -> Status: + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + usable_config = config + + packets_list = encode_packet( + raw_data='', + version=version, + sequence_number=sequence_number, + packet_type=usable_config.commands.PACKET_TYPE.ABORT, + ) + + if len(packets_list) == 0: + raise Exception('Cound not create packets') + + if len(packets_list) > 1: + raise Exception('Abort command has multiple packets') + + logger.debug('Sending abort') + + first_error: Optional[Exception] = None + + tries = 1 + inner_max_tries = max_tries + first_error = None + is_success = False + status: Optional[Status] = None + + packet = packets_list[0] + while tries <= inner_max_tries and not is_success: + try: + received_packet = await write_command( + connection=connection, + packet=packet, + version=version, + sequence_number=sequence_number, + ack_packet_types=[usable_config.commands.PACKET_TYPE.STATUS], + timeout=timeout, + ) + + payload_data_result = decode_payload_data( + received_packet['payload_data'], + version, + ) + protobuf_data = payload_data_result['protobuf_data'] + status = Status() + status.ParseFromString(hex_to_uint8array(protobuf_data)) + + if status.current_cmd_seq != sequence_number: + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + + is_success = True + except Exception as e: + # Don't retry if connection closed + if not can_retry(e): + tries = inner_max_tries + + if not first_error: + first_error = e + tries += 1 + + if not is_success and first_error: + raise first_error + + if not status: + raise Exception('Did not found status') + + await wait_for_idle(connection=connection, version=version) + + return status \ No newline at end of file diff --git a/packages/core/src/core/operations/proto/sendquery.py b/packages/core/src/core/operations/proto/sendquery.py new file mode 100644 index 0000000..8b1a2bb --- /dev/null +++ b/packages/core/src/core/operations/proto/sendquery.py @@ -0,0 +1,40 @@ +from typing import Optional +from interfaces import IDeviceConnection +from util.utils.assert_utils import assert_condition +from util.utils.crypto import uint8array_to_hex +from ...utils.logger import logger +from ...utils.packetversion import PacketVersion +from ...encoders.proto.generated.core_pb2 import Msg, Command +from ...operations.helpers.sendcommand import send_command as send_command_helper + + +async def send_query( + connection: IDeviceConnection, + applet_id: int, + data: bytes, + version: PacketVersion, + sequence_number: int, + max_tries: int = 5, + timeout: Optional[int] = None, +) -> None: + assert_condition(applet_id, 'Invalid appletId') + assert_condition(data, 'Invalid data') + + assert_condition(applet_id >= 0, 'appletId cannot be negative') + assert_condition(len(data) > 0, 'data cannot be empty') + + raw_data = uint8array_to_hex(data) + logger.debug('Sending query', {'appletId': applet_id, 'rawData': raw_data}) + + msg = Msg(cmd=Command(applet_id=applet_id)) + msg_data = uint8array_to_hex(msg.SerializeToString()) + + return await send_command_helper( + connection=connection, + proto_data=msg_data, + raw_data=raw_data, + version=version, + max_tries=max_tries, + sequence_number=sequence_number, + timeout=timeout, + ) \ No newline at end of file diff --git a/packages/core/src/core/operations/proto/waitForResult.py b/packages/core/src/core/operations/proto/waitForResult.py new file mode 100644 index 0000000..e0f662f --- /dev/null +++ b/packages/core/src/core/operations/proto/waitForResult.py @@ -0,0 +1,97 @@ +from typing import Optional, Callable, Dict, Any +from interfaces import IDeviceConnection +from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType +from interfaces.errors.compatibility_error import DeviceCompatibilityError, DeviceCompatibilityErrorType +from util.utils.assert_utils import assert_condition +from util.utils.crypto import uint8array_to_hex +from util.utils.sleep import sleep +from ...utils.logger import logger +from ...utils.packetversion import PacketVersion, PacketVersionMap +from ...encoders.proto.generated.core_pb2 import CmdState, DeviceIdleState, Status +from .getresult import get_result + + +class IWaitForCommandOutputParams: + def __init__( + self, + connection: IDeviceConnection, + sequence_number: int, + applet_id: int, + on_status: Optional[Callable[[Status], None]] = None, + version: PacketVersion = None, + options: Optional[Dict[str, Any]] = None, + allow_core_data: Optional[bool] = None, + ): + self.connection = connection + self.sequence_number = sequence_number + self.applet_id = applet_id + self.on_status = on_status + self.version = version + self.options = options + self.allow_core_data = allow_core_data + + +async def wait_for_result( + connection: IDeviceConnection, + sequence_number: int, + applet_id: int, + on_status: Optional[Callable[[Status], None]] = None, + options: Optional[Dict[str, Any]] = None, + version: PacketVersion = None, + allow_core_data: Optional[bool] = None, +) -> bytes: + assert_condition(connection, 'Invalid connection') + assert_condition(sequence_number, 'Invalid sequenceNumber') + assert_condition(applet_id, 'Invalid appletId') + assert_condition(version, 'Invalid version') + + assert_condition(applet_id >= 0, 'appletId cannot be negative') + + if version != PacketVersionMap.v3: + raise DeviceCompatibilityError( + DeviceCompatibilityErrorType.INVALID_SDK_OPERATION + ) + + while True: + response = await get_result( + connection=connection, + version=version, + applet_id=applet_id, + max_tries=options.get('maxTries', 5) if options else 5, + sequence_number=sequence_number, + timeout=options.get('timeout') if options else None, + allow_core_data=allow_core_data, + ) + + if not response['is_status']: + resp = response['result'] + + logger.debug('Received result', { + 'result': uint8array_to_hex(resp), + 'appletId': applet_id, + }) + + return resp + + status = response['result'] + + if ( + status.device_idle_state == DeviceIdleState.DEVICE_IDLE_STATE_DEVICE or + status.current_cmd_seq != sequence_number + ): + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + + if status.cmd_state in [ + CmdState.CMD_STATE_DONE, + CmdState.CMD_STATE_FAILED, + CmdState.CMD_STATE_INVALID_CMD, + ]: + raise Exception( + 'Command status is done or rejected, but no output is received' + ) + + if status.device_idle_state == DeviceIdleState.DEVICE_IDLE_STATE_USB: + if on_status: + on_status(status) + + await sleep(options.get('interval', 200) if options else 200) \ No newline at end of file diff --git a/packages/core/src/core/operations/proto/waitforidle.py b/packages/core/src/core/operations/proto/waitforidle.py new file mode 100644 index 0000000..75761c7 --- /dev/null +++ b/packages/core/src/core/operations/proto/waitforidle.py @@ -0,0 +1,117 @@ +from typing import Optional +import asyncio +from interfaces import IDeviceConnection +from interfaces.errors.connection_error import ( + DeviceConnectionError, + DeviceConnectionErrorType, +) +from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType +from ...encoders.proto.generated.core_pb2 import DeviceIdleState +from ...utils.packetversion import PacketVersion +from core.config import v3 as config +from ...utils.logger import logger +from .getstatus import get_status + + +async def wait_for_idle( + connection: IDeviceConnection, + version: PacketVersion, + timeout: Optional[int] = None, +) -> None: + async def promise_executor(): + try: + logger.debug("Waiting for device to be idle") + is_completed = False + + usable_config = config + timeout_id: Optional[asyncio.Task] = None + recheck_timeout_id: Optional[asyncio.Task] = None + + def clean_up(): + nonlocal is_completed + is_completed = True + if timeout_id: + timeout_id.cancel() + if recheck_timeout_id: + recheck_timeout_id.cancel() + + def set_recheck_timeout(): + nonlocal recheck_timeout_id + if is_completed: + return + + if recheck_timeout_id: + recheck_timeout_id.cancel() + + recheck_timeout_id = asyncio.create_task( + asyncio.sleep(usable_config.constants.IDLE_RECHECK_TIME / 1000) + ) + + async def recheck_if_idle(): + try: + if not await connection.is_connected(): + clean_up() + raise DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + ) + + if is_completed: + return + + status = await get_status( + connection=connection, + version=version, + dont_log=True, + ) + + if ( + status.device_idle_state + != DeviceIdleState.DEVICE_IDLE_STATE_USB + ): + clean_up() + return + + set_recheck_timeout() + except Exception as error: + if hasattr(error, "code") and error.code in [ + e.value for e in DeviceConnectionErrorType + ]: + clean_up() + raise error + + logger.error("Error while rechecking if idle") + logger.error(error) + set_recheck_timeout() + + async def timeout_handler(): + await asyncio.sleep( + ( + timeout + if timeout is not None + else usable_config.constants.IDLE_TIMEOUT + ) + / 1000 + ) + clean_up() + + if not await connection.is_connected(): + raise DeviceConnectionError( + DeviceConnectionErrorType.CONNECTION_CLOSED + ) + else: + raise DeviceAppError(DeviceAppErrorType.EXECUTING_OTHER_COMMAND) + + timeout_id = asyncio.create_task(timeout_handler()) + set_recheck_timeout() + + while not is_completed: + if recheck_timeout_id and recheck_timeout_id.done(): + await recheck_if_idle() + if not is_completed: + set_recheck_timeout() + await asyncio.sleep(0.01) + + except Exception as error: + raise error + + await promise_executor() diff --git a/packages/core/src/core/sdk.py b/packages/core/src/core/sdk.py index ef4670f..a769f54 100644 --- a/packages/core/src/core/sdk.py +++ b/packages/core/src/core/sdk.py @@ -20,10 +20,10 @@ from .utils.feature_map import FeatureName, is_feature_enabled from .types import IFeatureSupport, ISDK from .deprecated import DeprecatedCommunication -from .encoders.proto.types import DeviceIdleState +from .encoders.proto.generated.core_pb2 import DeviceIdleState from .encoders.raw.types import DeviceIdleState as RawDeviceIdleState from .utils.logger import logger -from .encoders.proto.generated.core import AppVersionResultResponse +from .encoders.proto.generated.version_pb2 import AppVersionResultResponse from interfaces.errors.app_error import DeviceAppError, DeviceAppErrorType @@ -281,12 +281,10 @@ async def get_app_versions( DeviceCompatibilityErrorType.DEVICE_NOT_SUPPORTED, ), ) - if not await self.is_supported(): raise DeviceCompatibilityError( DeviceCompatibilityErrorType.INVALID_SDK_OPERATION, ) - if not self.app_versions_map: result = await commands.get_app_versions( commands.GetAppVersionsParams( @@ -338,7 +336,6 @@ async def check_app_compatibility( options: Optional[Dict[str, Any]] = None, ) -> None: app_versions_result = await self.get_app_versions(None, options) - app_version_result = None for app in app_versions_result.app_versions: if app.id == self.applet_id: diff --git a/packages/core/src/core/services/session.py b/packages/core/src/core/services/session.py index 3a5858f..9164312 100644 --- a/packages/core/src/core/services/session.py +++ b/packages/core/src/core/services/session.py @@ -28,7 +28,6 @@ async def initiate_server_session( res = await http.post(f"{BASE_URL}/create", body) - print({"res": res}) return res["data"] diff --git a/packages/core/src/core/types.py b/packages/core/src/core/types.py index 6751dc3..62bb608 100644 --- a/packages/core/src/core/types.py +++ b/packages/core/src/core/types.py @@ -12,8 +12,8 @@ from interfaces import DeviceState, IDeviceConnection from .utils.packetversion import PacketVersion from .encoders.raw.types import RawData, StatusData -from .encoders.proto.generated.core import AppVersionResultResponse -from .encoders.proto.generated.common import Version +from .encoders.proto.generated.version_pb2 import AppVersionResultResponse +from .encoders.proto.generated.common_pb2 import Version class IDeprecatedCommunication(Protocol): diff --git a/packages/core/src/core/utils/common_error.py b/packages/core/src/core/utils/common_error.py index 2c1ba0a..2b40659 100644 --- a/packages/core/src/core/utils/common_error.py +++ b/packages/core/src/core/utils/common_error.py @@ -1,7 +1,7 @@ from typing import TypeVar, Optional, Dict, Any from interfaces.errors.app_error import DeviceAppErrorType, DeviceAppError from util.utils.assert_utils import assert_condition -from ..encoders.proto.generated.error import CommonError +from ..encoders.proto.generated.error_pb2 import CommonError T = TypeVar("T") diff --git a/packages/hw_hid/src/hw_hid/device_connection.py b/packages/hw_hid/src/hw_hid/device_connection.py index 5aa4bfb..d451b30 100644 --- a/packages/hw_hid/src/hw_hid/device_connection.py +++ b/packages/hw_hid/src/hw_hid/device_connection.py @@ -37,13 +37,70 @@ def __init__(self, device: IDevice, connection: Any): # pylint: disable=no-self-use async def get_connection_type(self) -> str: - return ConnectionTypeMap.HID + return ConnectionTypeMap.HID.value @staticmethod async def connect(device: IDevice): # Create HID device connection - connection = hid.device() # type: ignore - await asyncio.to_thread(connection.open_path, device["path"]) + # Check which API is available + connection = None + device_path = device["path"] + # Ensure path is bytes if it's a string (hidapi may need bytes) + if isinstance(device_path, str): + device_path_bytes = device_path.encode("utf-8") + else: + device_path_bytes = device_path + + if hasattr(hid, "Device"): + # Newer hidapi API - try different initialization methods + # Note: hid.Device() requires vid/pid or path parameter + try: + # Try passing path as bytes to constructor (most specific) + connection = await asyncio.to_thread(hid.Device, path=device_path_bytes) # type: ignore + except (TypeError, ValueError): + try: + # Try with string path + connection = await asyncio.to_thread(hid.Device, path=device["path"]) # type: ignore + except (TypeError, ValueError): + # Fallback: try with vid/pid if available + if "vendor_id" in device and "product_id" in device: + try: + connection = await asyncio.to_thread( + hid.Device, device["vendor_id"], device["product_id"] # type: ignore + ) + except (TypeError, ValueError, AttributeError) as e: + raise RuntimeError( + f"Failed to create HID device connection: {e}. " + "Tried: Device(path=bytes), Device(path=str), and Device(vid, pid). " + "Please check your hidapi installation." + ) + else: + raise RuntimeError( + "Failed to create HID device connection: " + "No path or vid/pid available. " + "Tried: Device(path=bytes) and Device(path=str). " + "Please check your hidapi installation." + ) + elif hasattr(hid, "device"): + # Older hidapi API + connection = hid.device() # type: ignore + try: + await asyncio.to_thread(connection.open_path, device_path_bytes) + except (TypeError, AttributeError): + await asyncio.to_thread(connection.open_path, device["path"]) + elif hasattr(hid, "open_path"): + # Alternative API: open_path as module-level function + try: + connection = await asyncio.to_thread(hid.open_path, device_path_bytes) # type: ignore + except (TypeError, ValueError): + connection = await asyncio.to_thread(hid.open_path, device["path"]) # type: ignore + else: + raise RuntimeError( + "HID library does not support device creation. " + "The 'hid' module is missing required methods (Device, device, or open_path). " + "Please reinstall the 'hidapi' package: " + "poetry run pip install --force-reinstall --no-cache-dir hid" + ) return DeviceConnection(device, connection) @staticmethod @@ -56,10 +113,8 @@ async def create(): if not devices: raise DeviceConnectionError(DeviceConnectionErrorType.NOT_CONNECTED) device_to_connect = devices[0] - # Create HID device connection - connection = hid.device() # type: ignore - await asyncio.to_thread(connection.open_path, device_to_connect["path"]) - return DeviceConnection(device_to_connect, connection) + # Create HID device connection using connect() method + return await DeviceConnection.connect(device_to_connect) @staticmethod async def get_available_connection(): @@ -108,7 +163,7 @@ async def receive(self) -> Optional[bytearray]: return bytearray(result) if result is not None else None async def peek(self) -> List[PoolData]: - return await self.data_listener.peek() + return self.data_listener.peek() def on_close(self): self.is_port_open = False diff --git a/packages/hw_hid/src/hw_hid/helpers/data_listeners.py b/packages/hw_hid/src/hw_hid/helpers/data_listeners.py index 74475c3..b076e5a 100644 --- a/packages/hw_hid/src/hw_hid/helpers/data_listeners.py +++ b/packages/hw_hid/src/hw_hid/helpers/data_listeners.py @@ -16,7 +16,6 @@ def __init__(self, params: Dict[str, Any]): self.device: IDevice = params["device"] self.on_close_callback = params.get("on_close") self.on_error_callback = params.get("on_error") - self.on_some_device_disconnect_binded = self.on_some_device_disconnect self.listening = False self.pool: [PoolData] = [] @@ -103,7 +102,9 @@ async def _read_data(self): def _read_hid_data(self): try: - return self.connection.read(64, timeout_ms=100) + # hidapi Device.read() only accepts size parameter + # No timeout parameter is supported - read is blocking + return self.connection.read(64) except Exception as error: if self.on_error_callback: self.on_error_callback(error) @@ -126,28 +127,33 @@ def on_error(self, error: Exception): def _run_device_monitor(self): while not self._stop_event.is_set(): - asyncio.run(self._check_device_connection()) + try: + self._check_device_connection_sync() + except Exception as e: + logger.error(f"Error in device monitor: {e}") time.sleep(1) - async def _check_device_connection(self): + def _check_device_connection_sync(self): + """Synchronous device connection check for use in thread.""" try: - await self.on_some_device_disconnect() + import hid + # Use synchronous hid.enumerate() directly + all_hid_devices = hid.enumerate() + + # Check if our device is still connected + is_device_connected = any( + device_info.get("path") and + device_info.get("path").decode("utf-8") == self.device["path"] + and device_info.get("vendor_id") == self.device.get("vendor_id") + and device_info.get("product_id") == self.device.get("product_id") + and device_info.get("serial_number") == self.device.get("serial") + for device_info in all_hid_devices + ) + + if not is_device_connected: + # Schedule async cleanup in the main event loop + # We can't await here, so we'll just log and let the connection + # error handling deal with it + logger.warn("Device appears to be disconnected") except Exception as e: - logger.error(f"Error in device monitor: {e}") - - async def on_some_device_disconnect(self): - connected_devices = await get_available_devices() - - is_device_connected = any( - d["path"] == self.device["path"] - and d["serial"] == self.device["serial"] - and d["product_id"] == self.device["product_id"] - and d["type"] == self.device["type"] - and d["device_state"] == self.device["device_state"] - and d["vendor_id"] == self.device["vendor_id"] - for d in connected_devices - ) - - if not is_device_connected: - await self.destroy() - await self.on_close() + logger.error(f"Error checking device connection: {e}") diff --git a/packages/hw_webusb/src/hw_webusb/device_connection.py b/packages/hw_webusb/src/hw_webusb/device_connection.py index 0d15592..728132a 100644 --- a/packages/hw_webusb/src/hw_webusb/device_connection.py +++ b/packages/hw_webusb/src/hw_webusb/device_connection.py @@ -26,7 +26,7 @@ def __init__(self, connection: usb.core.Device, data_listener: DataListener): self.initialized: bool = True async def get_connection_type(self) -> str: - return ConnectionTypeMap.WEBUSB + return ConnectionTypeMap.WEBUSB.value @staticmethod async def connect(connection: usb.core.Device): diff --git a/packages/interfaces/src/interfaces/errors/card_error.py b/packages/interfaces/src/interfaces/errors/card_error.py index df1a7bd..94729f3 100644 --- a/packages/interfaces/src/interfaces/errors/card_error.py +++ b/packages/interfaces/src/interfaces/errors/card_error.py @@ -1,5 +1,5 @@ from enum import Enum -from core.encoders.proto.generated.types import CardError +from core.encoders.proto.generated.error_pb2 import CardError from .sub_error import SubErrorToMap, SubErrorDetail diff --git a/packages/util/src/util/utils/create_status_listener.py b/packages/util/src/util/utils/create_status_listener.py index 2ecd23f..637a382 100644 --- a/packages/util/src/util/utils/create_status_listener.py +++ b/packages/util/src/util/utils/create_status_listener.py @@ -1,5 +1,6 @@ from typing import Dict, Any, Optional, Callable, List, TypedDict, Type from enum import Enum +from google.protobuf.internal.enum_type_wrapper import EnumTypeWrapper from interfaces.logger import ILogger from .crypto import num_to_byte_array @@ -17,21 +18,35 @@ class CreateStatusListenerParams(TypedDict): def get_numbers_from_enums(enums: Type[Enum]) -> List[int]: - return sorted( - [ + try: + return [ member.value for member in enums if isinstance(member.value, int) and member.value >= 0 ] - ) - + except Exception as e: + return enums.values() def get_names_from_enums(enums: Type[Enum]) -> List[str]: - return [member.name for member in enums] + try: + return [member.name for member in enums] + except Exception as e: + return enums.keys() + +def create_dict_from_enums(enums: Type[Enum]) -> Dict[str, int]: + values = get_numbers_from_enums(enums) + keys = get_names_from_enums(enums) + return dict(zip(keys, values)) + +def get_numbers_from_dict(enums: Dict[str, int]) -> List[int]: + return list(enums.values()) +def get_names_from_dict(enums: Dict[str, int]) -> List[str]: + return list(enums.keys()) def create_status_listener(params: CreateStatusListenerParams) -> Dict[str, Any]: enums: Type[Enum] = params["enums"] + enum_dict: Dict[str, int] = create_dict_from_enums(enums) on_event: Optional[EventCallback] = params.get("onEvent") logger: Optional[ILogger] = params.get("logger") _operation_enums: Optional[Type[Enum]] = params.get("operationEnums") @@ -42,18 +57,18 @@ def create_status_listener(params: CreateStatusListenerParams) -> Dict[str, Any] ) already_sent: Dict[int, bool] = {} - event_list = get_numbers_from_enums(enums) + event_list = get_numbers_from_dict(enum_dict) seed_generation_event_list = ( get_numbers_from_enums(seed_generation_enums) if seed_generation_enums else [] ) - operation_event_names = get_names_from_enums(operation_enums) + operation_event_names = get_names_from_dict(enum_dict) operation_seed_generation_event_name: Optional[str] = next( (e for e in operation_event_names if "SEED_GENERATED" in e), None ) def on_status(status: Dict[str, int]) -> None: - flow_status = status.get("flowStatus", 0) + flow_status = getattr(status, "flow_status", 0) byte_array = num_to_byte_array(flow_status) operation_status = byte_array[-1] if byte_array else 0 core_status = byte_array[-2] if len(byte_array) > 1 else 0 @@ -100,9 +115,9 @@ def on_status(status: Dict[str, int]) -> None: if logger: event_name = next( ( - member.name - for member in enums - if member.value == event_index + key + for key, value in enum_dict.items() + if value == event_index ), str(event_index), ) @@ -122,9 +137,9 @@ def force_status_update(flow_status: int) -> None: # Find the enum member name corresponding to the event_index event_name = next( ( - member.name - for member in enums - if member.value == event_index + key + for key, value in enum_dict.items() + if value == event_index ), str(event_index), ) diff --git a/poetry.lock b/poetry.lock index 2c2b53c..41d15c6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "anyio" @@ -6,6 +6,7 @@ version = "4.10.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, @@ -25,6 +26,7 @@ version = "1.5.1" description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67"}, {file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"}, @@ -36,18 +38,19 @@ version = "25.3.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, ] [package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] [[package]] name = "base58" @@ -55,6 +58,7 @@ version = "2.1.1" description = "Base58 and Base58Check implementation." optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2"}, {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"}, @@ -69,6 +73,7 @@ version = "1.2.0" description = "Reference implementation for Bech32 and segwit addresses." optional = false python-versions = ">=3.5" +groups = ["main"] files = [ {file = "bech32-1.2.0-py3-none-any.whl", hash = "sha256:990dc8e5a5e4feabbdf55207b5315fdd9b73db40be294a19b3752cde9e79d981"}, {file = "bech32-1.2.0.tar.gz", hash = "sha256:7d6db8214603bd7871fcfa6c0826ef68b85b0abd90fa21c285a9c5e21d2bd899"}, @@ -80,6 +85,7 @@ version = "1.2.5" description = "A better Protobuf / gRPC generator & library" optional = false python-versions = ">=3.6" +groups = ["main", "dev"] files = [ {file = "betterproto-1.2.5.tar.gz", hash = "sha256:74a3ab34646054f674d236d1229ba8182dc2eae86feb249b8590ef496ce9803d"}, ] @@ -100,6 +106,7 @@ version = "0.7.5" description = "Bitcoin cryptocurrency Library" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "bitcoinlib-0.7.5-py3-none-any.whl", hash = "sha256:3c273f0805d7aa08f292b1d4553c083bf8f1f72f4ec29479df2e05b95965c8d4"}, {file = "bitcoinlib-0.7.5.tar.gz", hash = "sha256:5de91c468e77144d5f3ab530c6a5b44a6e7d5a40970162f00c509861bb640da6"}, @@ -114,7 +121,7 @@ requests = ">=2.25.0" SQLAlchemy = ">=2.0.20" [package.extras] -dev = ["Cython (>=3.0.8)", "coveralls (>=4.0.1)", "mysql-connector-python (>=8.4.0)", "mysqlclient (>=2.2.0)", "psycopg (>=3.1.16)", "scrypt (>=0.8.20)", "sphinx (>=7.1.0)", "sphinx (>=7.2.0)", "sphinx_rtd_theme (>=2.0.0)", "win-unicode-console"] +dev = ["Cython (>=3.0.8)", "coveralls (>=4.0.1)", "mysql-connector-python (>=8.4.0)", "mysqlclient (>=2.2.0)", "psycopg (>=3.1.16)", "scrypt (>=0.8.20) ; platform_system != \"Windows\"", "sphinx (>=7.1.0) ; python_version < \"3.9\"", "sphinx (>=7.2.0) ; python_version >= \"3.9\"", "sphinx_rtd_theme (>=2.0.0)", "win-unicode-console ; platform_system == \"Windows\""] [[package]] name = "black" @@ -122,6 +129,7 @@ version = "24.10.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"}, {file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"}, @@ -166,6 +174,7 @@ version = "2023.7.12" description = "A library for 'bitcoin cryptography'" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "btclib-2023.7.12-py3-none-any.whl", hash = "sha256:3d790055f9a9ffccb10f867212db08563da6d1fe7bc09df55c822eaab18c9d5a"}, {file = "btclib-2023.7.12.tar.gz", hash = "sha256:f6501d60d24fe9b5548796d31ac7401b79894e48febc82325083c263f46dd125"}, @@ -180,6 +189,7 @@ version = "0.4.0" description = "Simple python bindings to libsecp256k1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "btclib_libsecp256k1-0.4.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:be8ae97b26c94557a3f45e95751cdb375b78c51a7b49a442bcc55c6995ee7a2f"}, {file = "btclib_libsecp256k1-0.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6fc6f0f7c24b0170b377890536353b149ad0d0301617cc679a22154c9b99feb"}, @@ -222,6 +232,7 @@ version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -233,6 +244,7 @@ version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, @@ -329,6 +341,7 @@ version = "3.4.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, @@ -417,6 +430,7 @@ version = "8.2.1" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, @@ -431,6 +445,7 @@ version = "20.0.0" description = "Cross-platform Python CFFI bindings for libsecp256k1" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "coincurve-20.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d559b22828638390118cae9372a1bb6f6594f5584c311deb1de6a83163a0919b"}, {file = "coincurve-20.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33d7f6ebd90fcc550f819f7f2cce2af525c342aac07f0ccda46ad8956ad9d99b"}, @@ -497,6 +512,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -508,6 +525,7 @@ version = "41.0.7" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:3c78451b78313fa81607fa1b3f1ae0a5ddd8014c38a02d9db0616133987b9cdf"}, {file = "cryptography-41.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:928258ba5d6f8ae644e764d0f996d61a8777559f72dfeb2eea7e2fe0ad6e782d"}, @@ -553,6 +571,7 @@ version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, @@ -571,6 +590,8 @@ version = "3.0.1" description = "Fast elliptic curve digital signatures" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "platform_system != \"Windows\"" files = [ {file = "fastecdsa-3.0.1-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:5793ac17891bc8c7002c8364b5df32ac17b486a68c6e32d941f3f0b3641b417b"}, {file = "fastecdsa-3.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e99b2695f3cbf363fa4fc12e036b5a2b50a01004b82972dce102bb73fa1833bd"}, @@ -601,6 +622,8 @@ version = "3.2.4" description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" files = [ {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, @@ -610,6 +633,8 @@ files = [ {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f47617f698838ba98f4ff4189aef02e7343952df3a615f847bb575c3feb177a7"}, + {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:af41be48a4f60429d5cad9d22175217805098a9ef7c40bfef44f7669fb9d74d8"}, {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, @@ -619,6 +644,8 @@ files = [ {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c"}, + {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5"}, {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, @@ -628,6 +655,8 @@ files = [ {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0"}, + {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d"}, {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, @@ -637,6 +666,8 @@ files = [ {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b"}, + {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929"}, {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, @@ -644,6 +675,8 @@ files = [ {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269"}, + {file = "greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681"}, {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, @@ -653,6 +686,8 @@ files = [ {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:28a3c6b7cd72a96f61b0e4b2a36f681025b60ae4779cc73c1535eb5f29560b10"}, + {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:52206cd642670b0b320a1fd1cbfd95bca0e043179c1d8a045f2c6109dfe973be"}, {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, @@ -668,6 +703,7 @@ version = "0.4.8" description = "Pure-Python gRPC implementation for asyncio" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654"}, {file = "grpclib-0.4.8.tar.gz", hash = "sha256:d8823763780ef94fed8b2c562f7485cf0bbee15fc7d065a640673667f7719c9a"}, @@ -686,6 +722,7 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -697,6 +734,7 @@ version = "4.2.0" description = "Pure-Python HTTP/2 protocol implementation" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, @@ -712,6 +750,7 @@ version = "1.0.8" description = "ctypes bindings for hidapi" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "hid-1.0.8-py3-none-any.whl", hash = "sha256:9b6fac92098322ae5d8d839c2bb30620b7533284bcf5f3e6aedcc0483c388046"}, {file = "hid-1.0.8.tar.gz", hash = "sha256:5ca129a7b9434ace5e3e429c1092a16792feffaf067a46b666e9c586872cdcfe"}, @@ -723,6 +762,7 @@ version = "4.1.0" description = "Pure-Python HPACK header encoding" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, @@ -734,6 +774,7 @@ version = "0.17.3" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"}, {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"}, @@ -755,6 +796,7 @@ version = "0.24.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"}, {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"}, @@ -767,7 +809,7 @@ idna = "*" sniffio = "*" [package.extras] -brotli = ["brotli", "brotlicffi"] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] @@ -778,6 +820,7 @@ version = "6.1.0" description = "Pure-Python HTTP/2 framing" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, @@ -789,6 +832,7 @@ version = "6.137.1" description = "A library for property-based testing" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "hypothesis-6.137.1-py3-none-any.whl", hash = "sha256:7cbda6a98ed4d32aad31a5fc5bff5e119b9275fe2579a7b08863cba313a4b9be"}, {file = "hypothesis-6.137.1.tar.gz", hash = "sha256:b086e644456da79ad460fdaf8fbf90a41a661e8a4076232dd4ea64cfbc0d0529"}, @@ -799,7 +843,7 @@ attrs = ">=22.2.0" sortedcontainers = ">=2.1.0,<3.0.0" [package.extras] -all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.93)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.24)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2025.2)", "watchdog (>=4.0.0)"] +all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.93)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.24)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\"", "watchdog (>=4.0.0)"] cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"] codemods = ["libcst (>=0.3.16)"] crosshair = ["crosshair-tool (>=0.0.93)", "hypothesis-crosshair (>=0.0.24)"] @@ -814,7 +858,7 @@ pytest = ["pytest (>=4.6)"] pytz = ["pytz (>=2014.1)"] redis = ["redis (>=3.0.0)"] watchdog = ["watchdog (>=4.0.0)"] -zoneinfo = ["tzdata (>=2025.2)"] +zoneinfo = ["tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""] [[package]] name = "idna" @@ -822,6 +866,7 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -836,6 +881,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -847,6 +893,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -864,6 +911,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -934,6 +982,7 @@ version = "6.6.3" description = "multidict implementation" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817"}, {file = "multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140"}, @@ -1053,6 +1102,7 @@ version = "1.16.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "mypy-1.16.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b4f0fed1022a63c6fec38f28b7fc77fca47fd490445c69d0a66266c59dd0b88a"}, {file = "mypy-1.16.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:86042bbf9f5a05ea000d3203cf87aa9d0ccf9a01f73f71c58979eb9249f46d72"}, @@ -1106,6 +1156,7 @@ version = "1.1.0" description = "Type system extensions for programs checked with the mypy type checker." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, @@ -1117,6 +1168,7 @@ version = "1.26.4" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, @@ -1162,6 +1214,7 @@ version = "23.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" +groups = ["main", "dev"] files = [ {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, @@ -1173,6 +1226,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1184,6 +1238,7 @@ version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -1200,6 +1255,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -1211,22 +1267,22 @@ testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "protobuf" -version = "4.25.8" +version = "6.33.3" description = "" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev"] files = [ - {file = "protobuf-4.25.8-cp310-abi3-win32.whl", hash = "sha256:504435d831565f7cfac9f0714440028907f1975e4bed228e58e72ecfff58a1e0"}, - {file = "protobuf-4.25.8-cp310-abi3-win_amd64.whl", hash = "sha256:bd551eb1fe1d7e92c1af1d75bdfa572eff1ab0e5bf1736716814cdccdb2360f9"}, - {file = "protobuf-4.25.8-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:ca809b42f4444f144f2115c4c1a747b9a404d590f18f37e9402422033e464e0f"}, - {file = "protobuf-4.25.8-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:9ad7ef62d92baf5a8654fbb88dac7fa5594cfa70fd3440488a5ca3bfc6d795a7"}, - {file = "protobuf-4.25.8-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:83e6e54e93d2b696a92cad6e6efc924f3850f82b52e1563778dfab8b355101b0"}, - {file = "protobuf-4.25.8-cp38-cp38-win32.whl", hash = "sha256:27d498ffd1f21fb81d987a041c32d07857d1d107909f5134ba3350e1ce80a4af"}, - {file = "protobuf-4.25.8-cp38-cp38-win_amd64.whl", hash = "sha256:d552c53d0415449c8d17ced5c341caba0d89dbf433698e1436c8fa0aae7808a3"}, - {file = "protobuf-4.25.8-cp39-cp39-win32.whl", hash = "sha256:077ff8badf2acf8bc474406706ad890466274191a48d0abd3bd6987107c9cde5"}, - {file = "protobuf-4.25.8-cp39-cp39-win_amd64.whl", hash = "sha256:f4510b93a3bec6eba8fd8f1093e9d7fb0d4a24d1a81377c10c0e5bbfe9e4ed24"}, - {file = "protobuf-4.25.8-py3-none-any.whl", hash = "sha256:15a0af558aa3b13efef102ae6e4f3efac06f1eea11afb3a57db2901447d9fb59"}, - {file = "protobuf-4.25.8.tar.gz", hash = "sha256:6135cf8affe1fc6f76cced2641e4ea8d3e59518d1f24ae41ba97bcad82d397cd"}, + {file = "protobuf-6.33.3-cp310-abi3-win32.whl", hash = "sha256:b4046f9f2ede57ad5b1d9917baafcbcad42f8151a73c755a1e2ec9557b0a764f"}, + {file = "protobuf-6.33.3-cp310-abi3-win_amd64.whl", hash = "sha256:1fd18f030ae9df97712fbbb0849b6e54c63e3edd9b88d8c3bb4771f84d8db7a4"}, + {file = "protobuf-6.33.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:648b7b0144222eb06cf529a3d7b01333c5f30b4196773b682d388f04db373759"}, + {file = "protobuf-6.33.3-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:08a6ca12f60ba99097dd3625ef4275280f99c9037990e47ce9368826b159b890"}, + {file = "protobuf-6.33.3-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:642fce7187526c98683c79a3ad68e5d646a5ef5eb004582fe123fc9a33a9456b"}, + {file = "protobuf-6.33.3-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:6fa9b5f4baa12257542273e5e6f3c3d3867b30bc2770c14ad9ac8315264bf986"}, + {file = "protobuf-6.33.3-cp39-cp39-win32.whl", hash = "sha256:c46dcc47b243b299f4f7eabeed21929c07f0d36fffe2ea8431793b53c308ab80"}, + {file = "protobuf-6.33.3-cp39-cp39-win_amd64.whl", hash = "sha256:2756963dcfd414eba46bcbb341f0e2c652036e5d700f112b3bb90fa1a031893a"}, + {file = "protobuf-6.33.3-py3-none-any.whl", hash = "sha256:c2bf221076b0d463551efa2e1319f08d4cffcc5f0d864614ccd3d0e77a637794"}, + {file = "protobuf-6.33.3.tar.gz", hash = "sha256:c8794debeb402963fddff41a595e1f649bcd76616ba56c835645cab4539e810e"}, ] [[package]] @@ -1235,6 +1291,8 @@ version = "2.23" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] +markers = "implementation_name != \"PyPy\"" files = [ {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, @@ -1246,6 +1304,7 @@ version = "3.23.0" description = "Cryptographic library for Python" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] files = [ {file = "pycryptodome-3.23.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a176b79c49af27d7f6c12e4b178b0824626f40a7b9fed08f712291b6d54bf566"}, {file = "pycryptodome-3.23.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:573a0b3017e06f2cffd27d92ef22e46aa3be87a2d317a5abf7cc0e84e321bd75"}, @@ -1296,6 +1355,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -1310,6 +1370,7 @@ version = "3.5" description = "Python Serial Port Extension" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, @@ -1324,6 +1385,7 @@ version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, @@ -1345,6 +1407,7 @@ version = "1.1.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"}, {file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"}, @@ -1363,6 +1426,7 @@ version = "1.1.5" description = "A library for handling Bitcoin transactions and associated data" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "python_bitcointx-1.1.5-py3-none-any.whl", hash = "sha256:7d961afac69dde865140e65af47c4ecc7b73a0697ced27244345539d8e94c555"}, ] @@ -1373,6 +1437,7 @@ version = "1.3.1" description = "Easy USB access for Python" optional = false python-versions = ">=3.9.0" +groups = ["main"] files = [ {file = "pyusb-1.3.1-py3-none-any.whl", hash = "sha256:bf9b754557af4717fe80c2b07cc2b923a9151f5c08d17bdb5345dac09d6a0430"}, {file = "pyusb-1.3.1.tar.gz", hash = "sha256:3af070b607467c1c164f49d5b0caabe8ac78dbed9298d703a8dbf9df4052d17e"}, @@ -1384,6 +1449,7 @@ version = "2.32.5" description = "Python HTTP for Humans." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -1405,6 +1471,7 @@ version = "0.5.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, @@ -1432,6 +1499,7 @@ version = "3.0.4" description = "Python helper for Semantic Versioning (https://semver.org)" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "semver-3.0.4-py3-none-any.whl", hash = "sha256:9c824d87ba7f7ab4a1890799cec8596f15c1241cb473404ea1cb0c55e4b04746"}, {file = "semver-3.0.4.tar.gz", hash = "sha256:afc7d8c584a5ed0a11033af086e8af226a9c0b206f313e0301f8dd7b6b589602"}, @@ -1443,6 +1511,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1454,6 +1523,7 @@ version = "1.3.1" description = "Sniff out which async library your code is running under" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, @@ -1465,6 +1535,7 @@ version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, @@ -1476,6 +1547,7 @@ version = "2.0.43" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "SQLAlchemy-2.0.43-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:21ba7a08a4253c5825d1db389d4299f64a100ef9800e4624c8bf70d8f136e6ed"}, {file = "SQLAlchemy-2.0.43-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11b9503fa6f8721bef9b8567730f664c5a5153d25e247aadc69247c4bc605227"}, @@ -1571,6 +1643,7 @@ version = "1.2.0" description = "String case converter." optional = false python-versions = "*" +groups = ["main", "dev"] files = [ {file = "stringcase-1.2.0.tar.gz", hash = "sha256:48a06980661908efe8d9d34eab2b6c13aefa2163b3ced26972902e3bdfd87008"}, ] @@ -1581,6 +1654,7 @@ version = "4.14.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af"}, {file = "typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4"}, @@ -1592,18 +1666,19 @@ version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.11" -content-hash = "3332812477ebd62412962fa0f5ee8318e06fdfc1521110121bfb7bd54cf29d9b" +content-hash = "c6eac3d03ad7d2883cadd6e2282ced8780c4a8980a6ff1d9679051c65cf1edeb" diff --git a/pyproject.toml b/pyproject.toml index 3677104..aeb70cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ bech32 = "^1.2.0" # Cryptography cryptography = "^41.0.0" # Protobuf -protobuf = "^4.24.0" +protobuf = "^6.0.0" [tool.poetry.group.dev.dependencies] ruff = "^0.5.0" diff --git a/scripts/extract_types/__init__.py b/scripts/extract_types/__init__.py deleted file mode 100644 index 267af07..0000000 --- a/scripts/extract_types/__init__.py +++ /dev/null @@ -1,259 +0,0 @@ -#!/usr/bin/env python3 -import os -import sys -import re -from tsort import t_sort -from typing import Dict, List - -ignore_files = ["types.py", "__init__.py"] - - -def throw_invalid_usage(): - print( - "Invalid arguments. Usage: python extract_types.py " - ) - sys.exit(1) - - -if len(sys.argv) != 3: - throw_invalid_usage() - -root_path = sys.argv[1] -interface_file_path = sys.argv[2] - - -def count_chars(s, c): - return s.count(c) - - -global_interface_list: Dict[str, List[str]] = {} -global_enum_list: Dict[str, List[str]] = {} - - -def remember_interface(interface_list: Dict[str, List[str]]) -> None: - global global_interface_list - global_interface_list.update(interface_list) - - -def remember_enum(enum_list: Dict[str, List[str]]) -> None: - global global_enum_list - global_enum_list.update(enum_list) - - -def save_interfaces( - parsed_interfaces: Dict[str, Dict[str, List[str]]], tsort_edges: List[List[str]] -) -> None: - sorted_keys = t_sort(tsort_edges)[::-1] - - interface_file_data = [] - enum_file_data = [] - - for key in sorted_keys: - if key in parsed_interfaces: - interface_file_data.append("\n".join(parsed_interfaces[key]["data"])) - - for key in global_enum_list: - enum_file_data.append("\n".join(global_enum_list[key])) - - # Add proper imports at the top - imports = [ - "from dataclasses import dataclass", - "from typing import List, Optional, Union, Dict, Any", - "import betterproto", - "", - "# Generated by extract_types script", - "# Consolidates enums and interfaces from betterproto-generated files", - "", - ] - - file_data = ( - "\n".join(imports) - + "\n\n" - + "\n\n".join(enum_file_data) - + "\n\n" - + "".join(interface_file_data) - ) - - with open(interface_file_path, "w") as f: - f.write(file_data) - - -def parse_interfaces() -> None: - parsed_interfaces: Dict[str, Dict[str, List[str]]] = {} - interface_names: List[str] = list(global_interface_list.keys()) - original_interface_names: List[str] = [name[1:] for name in interface_names] - interface_block: List[str] = [] - tsort_edges: List[List[str]] = [] - - for interface_name in interface_names: - lines = global_interface_list[interface_name] - interface_block.append(lines[0]) - dependencies = [] - - for i in range(1, len(lines) - 1): - line = lines[i] - is_modified = False - - for idx, inner_interface_name in enumerate(original_interface_names): - pattern = rf"(\s*\w+\s*:\s*)({inner_interface_name})(\s*(?:\[.*?\])?(?:\s*\|\s*None)?)" - match = re.search(pattern, line) - - if match: - is_modified = True - interface_idx = original_interface_names.index(inner_interface_name) - dependencies.append(interface_names[interface_idx]) - - if interface_name == interface_names[interface_idx]: - print( - f"Warning: Cyclic dependency found: {interface_name} <=> {interface_names[interface_idx]}" - ) - else: - tsort_edges.append( - [interface_name, interface_names[interface_idx]] - ) - - new_line = f"{match.group(1)}{interface_names[interface_idx]}{match.group(3)}" - interface_block.append(new_line) - break - - if not is_modified: - interface_block.append(line) - - interface_block.append(lines[-1]) - interface_block.append("\n") - - parsed_interfaces[interface_name] = { - "data": interface_block, - "dependencies": dependencies, - } - - if len(interface_block) <= 3: - interface_block.insert(0, "# Empty interface") - - interface_block = [] - - save_interfaces(parsed_interfaces, tsort_edges) - - -def extract_interfaces_from_file(file_data: List[str]) -> None: - interface_list: Dict[str, List[str]] = {} - is_interface_open = False - interface_name = "" - interface_block: List[str] = [] - base_indent = 0 - - interface_start_regex = r"^(\s*)@dataclass\s*$" - class_regex = r"^(\s*)class\s+(\w+)\s*\(\s*betterproto\.Message\s*\):" - - for i, line in enumerate(file_data): - if is_interface_open: - if line.strip() == "": - interface_block.append(line) - elif line.strip() and len(line) - len(line.lstrip()) <= base_indent: - interface_list[interface_name] = interface_block - interface_name = "" - interface_block = [] - is_interface_open = False - if re.match(interface_start_regex, line) and i + 1 < len(file_data): - next_line = file_data[i + 1] - class_match = re.match(class_regex, next_line) - if class_match: - is_interface_open = True - base_indent = len(class_match.group(1)) - interface_name = f"I{class_match.group(2)}" - interface_block.append( - f"{class_match.group(1)}class {interface_name}:" - ) - else: - interface_block.append(line) - else: - if re.match(interface_start_regex, line) and i + 1 < len(file_data): - next_line = file_data[i + 1] - class_match = re.match(class_regex, next_line) - if class_match: - is_interface_open = True - base_indent = len(class_match.group(1)) - interface_name = f"I{class_match.group(2)}" - interface_block.append(line) - interface_block.append( - f"{class_match.group(1)}class {interface_name}:" - ) - - if is_interface_open and interface_name: - interface_list[interface_name] = interface_block - - remember_interface(interface_list) - - -def extract_enums_from_file(file_data: List[str]) -> None: - enum_list: Dict[str, List[str]] = {} - is_enum_open = False - enum_name = "" - enum_block: List[str] = [] - base_indent = 0 - - # betterproto generates enums as class X(betterproto.Enum): without @dataclass - class_regex = r"^(\s*)class\s+(\w+)\s*\(\s*betterproto\.Enum\s*\):" - - for i, line in enumerate(file_data): - if is_enum_open: - if line.strip() == "": - enum_block.append(line) - elif line.strip() and len(line) - len(line.lstrip()) <= base_indent: - enum_list[enum_name] = enum_block - enum_name = "" - enum_block = [] - is_enum_open = False - # Check for next enum - class_match = re.match(class_regex, line) - if class_match: - is_enum_open = True - base_indent = len(class_match.group(1)) - enum_name = class_match.group(2) - enum_block.append(line) - else: - enum_block.append(line) - else: - # Look for enum class directly (no @dataclass decorator) - class_match = re.match(class_regex, line) - if class_match: - is_enum_open = True - base_indent = len(class_match.group(1)) - enum_name = class_match.group(2) - enum_block.append(line) - - if is_enum_open and enum_name: - enum_list[enum_name] = enum_block - - remember_enum(enum_list) - - -def extract_types_from_file(file_path: str) -> None: - with open(file_path, "r") as f: - file_data = f.read().split("\n") - - extract_interfaces_from_file(file_data) - extract_enums_from_file(file_data) - - -def extract_types(root_path: str) -> None: - for root, dirs, files in os.walk(root_path): - for file in files: - if file in ignore_files: - continue - - file_path = os.path.join(root, file) - - if not os.path.isfile(file_path) or not file_path.endswith(".py"): - continue - - extract_types_from_file(file_path) - - -def run() -> None: - extract_types(root_path) - parse_interfaces() - - -if __name__ == "__main__": - run() diff --git a/scripts/extract_types/tsort.py b/scripts/extract_types/tsort.py deleted file mode 100644 index 94e27dc..0000000 --- a/scripts/extract_types/tsort.py +++ /dev/null @@ -1,50 +0,0 @@ -def t_sort(edges): - """ - Perform a topological sort on directed graph edges. - Returns a list of nodes in topological order. - """ - # Create a graph representation - graph = {} - nodes = set() - - # Add all nodes and edges to the graph - for edge in edges: - from_node, to_node = edge - nodes.add(from_node) - nodes.add(to_node) - - if from_node not in graph: - graph[from_node] = [] - graph[from_node].append(to_node) - - # Initialize all nodes in the graph - for node in nodes: - if node not in graph: - graph[node] = [] - - # Track visited nodes and result - visited = set() - temp_visited = set() - result = [] - - def visit(node): - if node in temp_visited: - raise ValueError(f"Cyclic dependency detected involving {node}") - if node in visited: - return - - temp_visited.add(node) - - for neighbor in graph.get(node, []): - visit(neighbor) - - temp_visited.remove(node) - visited.add(node) - result.append(node) - - # Visit all nodes - for node in nodes: - if node not in visited: - visit(node) - - return result diff --git a/scripts/fix_proto_imports.py b/scripts/fix_proto_imports.py new file mode 100755 index 0000000..752efac --- /dev/null +++ b/scripts/fix_proto_imports.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +""" +Post-process generated protobuf files to fix import statements. +This script fixes absolute imports to relative imports where needed. +""" + +import os +import re +import sys +from pathlib import Path + + +def fix_top_level_imports(file_path: Path) -> bool: + """Fix imports in top level generated files.""" + content = file_path.read_text() + original = content + + content = re.sub( + r"^import (\w+)_pb2 as (\w+)__pb2", + r"from . import \1_pb2 as \2__pb2", + content, + flags=re.MULTILINE, + ) + + if content != original: + file_path.write_text(content) + return True + return False + + +def fix_package_level_imports(file_path: Path, package_name: str) -> bool: + """Fix imports in package generated files.""" + content = file_path.read_text() + original = content + + # Fix package_name.* imports to relative imports + content = re.sub( + f"^from {package_name} import (\w+) as (\w+)", + r"from . import \1 as \2", + content, + flags=re.MULTILINE, + ) + + # Fix all other imports(importted from top-level files) to relative imports + content = re.sub( + r"^import (\w+)_pb2 as (\w+)__pb2", + r"from .. import \1_pb2 as \2__pb2", + content, + flags=re.MULTILINE, + ) + + if content != original: + file_path.write_text(content) + return True + return False + + +def process_directory(directory: Path, package_type: str): + """Process all _pb2.py files in a directory.""" + if not directory.exists(): + print(f"Warning: Directory {directory} does not exist") + return + + # first get the top level *_pb2.py files + top_level_pb2_files = list(directory.glob("*_pb2.py")) + for file_path in top_level_pb2_files: + if fix_top_level_imports(file_path): + print(f"Fixed imports in: {file_path}") + + # now get the package level *_pb2.py files + package_level_pb2_files = list(directory.glob(f"{package_type}/*_pb2.py")) + for file_path in package_level_pb2_files: + if fix_package_level_imports(file_path, package_type): + print(f"Fixed imports in: {file_path}") + + +def main(): + if len(sys.argv) < 3: + print("Usage: fix_proto_imports.py ") + print(" package_type: core, manager, or btc") + sys.exit(1) + + directory = Path(sys.argv[1]) + package_type = sys.argv[2] + + process_directory(directory, package_type) + print(f"Finished processing {package_type} package imports") + + +if __name__ == "__main__": + main() diff --git a/test_app/README.md b/test_app/README.md new file mode 100644 index 0000000..8afb34d --- /dev/null +++ b/test_app/README.md @@ -0,0 +1,295 @@ +# Cypherock SDK Test Application + +This test application allows you to test the Cypherock SDK packages with actual firmware/hardware devices. + +## Features + +- **Device Discovery**: Automatically discover devices via HID, Serial Port, or WebUSB +- **ManagerApp Tests**: Test device management operations (get device info, wallets, logs, etc.) +- **BtcApp Tests**: Test Bitcoin operations (get public keys, xpubs, sign transactions) +- **Flexible Connection**: Support for multiple connection types with automatic fallback + +## Usage + +### Quick Start (Recommended) + +**Important:** If `hidapi` is installed but Python can't find it, reinstall the Python package first: + +```bash +poetry run pip install --force-reinstall --no-cache-dir hid +``` + +Then use the helper script which automatically sets library paths: + +```bash +# List devices +./test_app/run.sh --list-devices + +# Run tests +./test_app/run.sh --test manager-device-info +``` + +**Note:** The test app will work with any available connection type (HID, Serial, or WebUSB). If HID is unavailable, it will automatically try Serial or WebUSB. + +### Direct Usage + +If running directly, you may need to set library paths first (macOS): + +```bash +# Set library path for macOS +export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH + +# Then run commands +poetry run python test_app/main.py --list-devices +``` + +### List Available Devices + +```bash +poetry run python test_app/main.py --list-devices +# Or use the helper script: +./test_app/run.sh --list-devices +``` + +### Run All Tests + +```bash +poetry run python test_app/main.py --test all +``` + +### Run ManagerApp Tests + +```bash +# Run all ManagerApp tests +poetry run python test_app/main.py --test manager-all + +# Run specific ManagerApp tests +poetry run python test_app/main.py --test manager-device-info +poetry run python test_app/main.py --test manager-wallets +poetry run python test_app/main.py --test manager-logs +poetry run python test_app/main.py --test manager-sdk-version +``` + +### Run BtcApp Tests + +```bash +# Run all BtcApp tests +poetry run python test_app/main.py --test btc-all + +# Get public key for a specific path +poetry run python test_app/main.py --test btc-public-key --btc-path "m/84'/0'/0'/0/0" + +# Get extended public keys +poetry run python test_app/main.py --test btc-xpubs --btc-paths "m/84'/0'/0'" "m/49'/0'/0'" + +# Sign a transaction (requires transaction hex) +poetry run python test_app/main.py --test btc-sign-txn --txn-hex "0100000001..." +``` + +### Connection Options + +```bash +# Use specific connection type +poetry run python test_app/main.py --test manager-device-info --connection hid +poetry run python test_app/main.py --test manager-device-info --connection serial +poetry run python test_app/main.py --test manager-device-info --connection webusb + +# Use specific device index (if multiple devices are connected) +poetry run python test_app/main.py --test manager-device-info --device-index 1 +``` + +## Available Tests + +### ManagerApp Tests + +- `manager-device-info`: Get device information +- `manager-wallets`: List all wallets on the device +- `manager-logs`: Get device logs +- `manager-select-wallet`: Select a wallet (may require user interaction) +- `manager-sdk-version`: Get SDK version and compatibility info +- `manager-all`: Run all ManagerApp tests + +### BtcApp Tests + +- `btc-public-key`: Get public key for a BIP32 path +- `btc-xpubs`: Get extended public keys for multiple paths +- `btc-sign-txn`: Sign a Bitcoin transaction +- `btc-all`: Run all BtcApp tests + +## Examples + +### Example 1: Quick Device Check + +```bash +# List devices and check SDK version +poetry run python test_app/main.py --list-devices +poetry run python test_app/main.py --test manager-sdk-version +``` + +### Example 2: Test Bitcoin Operations + +```bash +# Get public key for a native segwit address +poetry run python test_app/main.py --test btc-public-key --btc-path "m/84'/0'/0'/0/0" + +# Get xpubs for different address types +poetry run python test_app/main.py --test btc-xpubs \ + --btc-paths "m/84'/0'/0'" "m/49'/0'/0'" "m/44'/0'/0'" +``` + +### Example 3: Full Test Suite + +```bash +# Run all tests with automatic device detection +poetry run python test_app/main.py --test all +``` + +## Requirements + +- A Cypherock X1 hardware device connected via HID, Serial Port, or WebUSB +- Python 3.11+ +- All SDK dependencies installed (run `make setup`) + +### System Dependencies + +**macOS:** +```bash +brew install hidapi libusb +``` + +**Linux (Ubuntu/Debian):** +```bash +sudo apt-get install libhidapi-dev libusb-1.0-0-dev +``` + +**Windows:** +- Install libusb and hidapi libraries manually or via package manager + +## Troubleshooting + +### No devices found + +- Ensure the device is connected and powered on +- Check that the device is not in use by another application +- Try different connection types: `--connection hid`, `--connection serial`, or `--connection webusb` + +### Permission errors (Linux) + +You may need to add udev rules for USB devices: + +```bash +# Create udev rule for Cypherock devices +sudo nano /etc/udev/rules.d/99-cypherock.rules +``` + +Add: +``` +SUBSYSTEM=="usb", ATTR{idVendor}=="3503", MODE="0666" +``` + +Then reload: +```bash +sudo udevadm control --reload-rules +sudo udevadm trigger +``` + +### Missing system libraries + +If you see errors like "Unable to load libhidapi", install the required system libraries: + +**macOS:** +```bash +brew install hidapi libusb +# After installing, reinstall the Python hid package: +poetry run pip install --force-reinstall --no-cache-dir hid +``` + +**Linux:** +```bash +sudo apt-get install libhidapi-dev libusb-1.0-0-dev +# After installing, reinstall the Python hid package: +poetry run pip install --force-reinstall --no-cache-dir hid +``` + +**Note:** If `hidapi` is already installed but Python still can't find it, try: + +1. **Use the helper script** (easiest): + ```bash + ./test_app/run.sh --list-devices + ``` + +2. **Set library path manually** (macOS): + ```bash + export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH + poetry run python test_app/main.py --list-devices + ``` + +3. **Reinstall the Python `hid` package**: + ```bash + poetry run pip install --force-reinstall --no-cache-dir hid + ``` + +4. **Restart your terminal/Python environment** after installing system libraries + +### HID Connection Errors + +If you see errors like `module 'hid' has no attribute 'device'` when trying to connect via HID: + +This usually means the Python `hid` package (hidapi) is not properly installed or is missing required methods. Try: + +1. **Reinstall the hid package**: + ```bash + poetry run pip install --force-reinstall --no-cache-dir hid + ``` + +2. **Verify the system library is installed**: + ```bash + brew install hidapi # macOS + # or + sudo apt-get install libhidapi-dev # Linux + ``` + +3. **Try using WebUSB instead** (if available): + ```bash + ./test_app/run.sh --test manager-device-info --connection webusb + ``` + +4. **Check if devices are discoverable**: + ```bash + ./test_app/run.sh --list-devices + ``` + If devices are listed but connection fails, the issue is with the HID library installation. + +### USB Permission Errors (WebUSB) + +If you see errors like `[Errno 13] Access denied (insufficient permissions)` when using WebUSB: + +**macOS:** +1. **Recommended:** Use HID connection instead (better permission handling): + ```bash + ./test_app/run.sh --test manager-device-info --connection hid + ``` + +2. **Alternative:** Run with sudo (not recommended for development): + ```bash + sudo ./test_app/run.sh --test manager-device-info --connection webusb + ``` + +3. **Alternative:** Ensure libusb is properly installed: + ```bash + brew install libusb + # Then try again + ``` + +**Linux:** +Create a udev rule (see "Linux USB Permissions" section above). + +**Note:** HID connection typically has better permission handling and is recommended over WebUSB when available. + +### Connection errors + +- Try disconnecting and reconnecting the device +- Check if the device is in bootloader mode (some operations may not work) +- Use `--list-devices` to verify the device is detected +- If a specific connection type fails, try another: `--connection hid`, `--connection serial`, or `--connection webusb` + diff --git a/test_app/__init__.py b/test_app/__init__.py new file mode 100644 index 0000000..2955a2b --- /dev/null +++ b/test_app/__init__.py @@ -0,0 +1,2 @@ +"""Test application for Cypherock SDK with actual firmware.""" + diff --git a/test_app/device_helper.py b/test_app/device_helper.py new file mode 100644 index 0000000..2a661a4 --- /dev/null +++ b/test_app/device_helper.py @@ -0,0 +1,224 @@ +"""Helper module for device discovery and connection.""" +import asyncio +from typing import List, Optional, Tuple, Any + +from interfaces import IDevice, DeviceState, ConnectionTypeMap +from interfaces.errors.connection_error import DeviceConnectionError + +# Try to import connection modules, handle missing dependencies gracefully +HID_AVAILABLE = False +HID_ERROR = None +SERIAL_AVAILABLE = False +SERIAL_ERROR = None +WEBUSB_AVAILABLE = False +WEBUSB_ERROR = None + +try: + from hw_hid import DeviceConnection as HIDDeviceConnection + HID_AVAILABLE = True +except ImportError as e: + HID_ERROR = str(e) + +try: + from hw_serialport import DeviceConnection as SerialDeviceConnection + SERIAL_AVAILABLE = True +except ImportError as e: + SERIAL_ERROR = str(e) + +try: + from hw_webusb import DeviceConnection as WebUSBDeviceConnection + from hw_webusb.helpers.connection import create_port + WEBUSB_AVAILABLE = True +except ImportError as e: + WEBUSB_ERROR = str(e) + + +async def discover_devices() -> Tuple[List[IDevice], List[IDevice], bool]: + """ + Discover all available devices across all connection types. + + Returns: + Tuple of (hid_devices, serial_devices, webusb_available) + Note: WebUSB doesn't support listing, so we just check if one is available + """ + hid_devices = [] + serial_devices = [] + webusb_available = False + + if HID_AVAILABLE: + try: + hid_devices = await HIDDeviceConnection.list() + except Exception as e: + print(f"Error discovering HID devices: {e}") + else: + print(f"HID not available: {HID_ERROR or 'Library not installed'}") + + if SERIAL_AVAILABLE: + try: + serial_devices = await SerialDeviceConnection.list() + except Exception as e: + print(f"Error discovering Serial devices: {e}") + else: + print(f"Serial not available: {SERIAL_ERROR or 'Library not installed'}") + + if WEBUSB_AVAILABLE: + try: + # WebUSB doesn't have a list() method, so we try to create a port + # to see if any device is available + await create_port() + webusb_available = True + except Exception: + webusb_available = False + else: + print(f"WebUSB not available: {WEBUSB_ERROR or 'Library not installed'}") + + return hid_devices, serial_devices, webusb_available + + +def print_devices( + hid_devices: List[IDevice], + serial_devices: List[IDevice], + webusb_available: bool, +) -> None: + """Print all discovered devices in a formatted way.""" + print("\n=== Discovered Devices ===\n") + + if hid_devices: + print("HID Devices:") + for i, device in enumerate(hid_devices, 1): + print( + f" [{i}] {device.get('path', 'N/A')} - " + f"State: {device.get('device_state', 'N/A')} - " + f"Serial: {device.get('serial', 'N/A')}" + ) + else: + print("HID Devices: None found") + + if serial_devices: + print("\nSerial Port Devices:") + for i, device in enumerate(serial_devices, 1): + print( + f" [{i}] {device.get('path', 'N/A')} - " + f"State: {device.get('device_state', 'N/A')} - " + f"Serial: {device.get('serial', 'N/A')}" + ) + else: + print("\nSerial Port Devices: None found") + + if webusb_available: + print("\nWebUSB Devices: Available (1 device)") + else: + print("\nWebUSB Devices: None found") + + print() + + +async def create_connection( + connection_type: str = "auto", + device_index: int = 0, +) -> Tuple[Any, str]: + """ + Create a connection to a device. + + Args: + connection_type: Type of connection ('hid', 'serial', 'webusb', or 'auto') + device_index: Index of device to connect to (0-based) + + Returns: + Tuple of (connection, connection_type_string) + """ + hid_devices, serial_devices, webusb_available = await discover_devices() + + connection = None + conn_type = None + + if connection_type == "auto": + # Try HID first, then Serial, then WebUSB + if HID_AVAILABLE and hid_devices: + connection_type = "hid" + elif SERIAL_AVAILABLE and serial_devices: + connection_type = "serial" + elif WEBUSB_AVAILABLE and webusb_available: + connection_type = "webusb" + else: + available_types = [] + if HID_AVAILABLE: + available_types.append("HID") + if SERIAL_AVAILABLE: + available_types.append("Serial") + if WEBUSB_AVAILABLE: + available_types.append("WebUSB") + if not available_types: + raise DeviceConnectionError( + "No connection types available. Please install required system libraries:\n" + " macOS: brew install hidapi libusb\n" + " Linux: sudo apt-get install libhidapi-dev libusb-1.0-0-dev" + ) + raise DeviceConnectionError("No devices found") + + if connection_type == "hid": + if not HID_AVAILABLE: + error_msg = HID_ERROR or "Library not installed" + raise DeviceConnectionError( + f"HID connection not available. Error: {error_msg}\n" + "Install system library: brew install hidapi (macOS) or sudo apt-get install libhidapi-dev (Linux)" + ) + if not hid_devices or device_index >= len(hid_devices): + raise DeviceConnectionError( + f"No HID device at index {device_index}. Found {len(hid_devices)} devices." + ) + device = hid_devices[device_index] + connection = await HIDDeviceConnection.connect(device) + conn_type = ConnectionTypeMap.HID.value + + elif connection_type == "serial": + if not SERIAL_AVAILABLE: + error_msg = SERIAL_ERROR or "Library not installed" + raise DeviceConnectionError( + f"Serial connection not available. Error: {error_msg}" + ) + if not serial_devices or device_index >= len(serial_devices): + raise DeviceConnectionError( + f"No Serial device at index {device_index}. Found {len(serial_devices)} devices." + ) + device = serial_devices[device_index] + connection = await SerialDeviceConnection.connect(device) + conn_type = ConnectionTypeMap.SERIAL_PORT.value + + elif connection_type == "webusb": + if not WEBUSB_AVAILABLE: + error_msg = WEBUSB_ERROR or "Library not installed" + raise DeviceConnectionError( + f"WebUSB connection not available. Error: {error_msg}\n" + "Install system library: brew install libusb (macOS) or sudo apt-get install libusb-1.0-0-dev (Linux)" + ) + if not webusb_available: + raise DeviceConnectionError("No WebUSB device available") + # WebUSB uses create() which finds the first available device + try: + connection = await WebUSBDeviceConnection.create() + conn_type = ConnectionTypeMap.WEBUSB.value + except Exception as e: + error_msg = str(e) + # Check for permission errors + if "Access denied" in error_msg or "insufficient permissions" in error_msg.lower() or "errno 13" in error_msg.lower(): + raise DeviceConnectionError( + f"WebUSB permission denied: {error_msg}\n\n" + "To fix USB permissions on macOS:\n" + " 1. Try using HID connection instead: --connection hid\n" + " 2. Or run with sudo (not recommended): sudo ./test_app/run.sh ...\n" + " 3. Or install libusb with proper permissions:\n" + " brew install libusb\n" + " Then try running again.\n\n" + "Note: HID connection typically has better permission handling." + ) + raise DeviceConnectionError(f"Failed to create WebUSB connection: {error_msg}") + + else: + raise ValueError(f"Invalid connection type: {connection_type}") + + if not connection: + raise DeviceConnectionError("Failed to create connection") + + return connection, conn_type + diff --git a/test_app/main.py b/test_app/main.py new file mode 100755 index 0000000..8fab1d5 --- /dev/null +++ b/test_app/main.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +"""Main entry point for the Cypherock SDK test application.""" +import argparse +import asyncio +import sys + +from device_helper import discover_devices, print_devices, create_connection +from tests_manager import ( + test_get_device_info, + test_get_wallets, + test_get_logs, + test_select_wallet, + test_get_sdk_version, + run_all_manager_tests, +) +from tests_btc import ( + test_get_public_key, + test_get_xpubs, + test_sign_txn, + run_all_btc_tests, +) + + +async def main(): + """Main function to run the test application.""" + parser = argparse.ArgumentParser( + description="Cypherock SDK Test Application - Test packages with actual firmware" + ) + parser.add_argument( + "--connection", + choices=["auto", "hid", "serial", "webusb"], + default="auto", + help="Connection type to use (default: auto)", + ) + parser.add_argument( + "--device-index", + type=int, + default=0, + help="Device index to connect to (default: 0)", + ) + parser.add_argument( + "--list-devices", + action="store_true", + help="List all available devices and exit", + ) + + # Test selection + parser.add_argument( + "--test", + choices=[ + "all", + "manager-all", + "btc-all", + "manager-device-info", + "manager-wallets", + "manager-logs", + "manager-select-wallet", + "manager-sdk-version", + "btc-public-key", + "btc-xpubs", + "btc-sign-txn", + ], + help="Specific test to run", + ) + + # BTC-specific arguments + parser.add_argument( + "--btc-path", + type=str, + default="m/84'/0'/0'/0/0", + help="BIP32 path for BTC operations (default: m/84'/0'/0'/0/0)", + ) + parser.add_argument( + "--btc-paths", + nargs="+", + help="Multiple BIP32 paths for get_xpubs (e.g., --btc-paths \"m/84'/0'/0'\" \"m/49'/0'/0'\")", + ) + parser.add_argument( + "--txn-hex", + type=str, + help="Transaction hex for sign_txn test", + ) + + args = parser.parse_args() + + # List devices if requested + if args.list_devices: + print("Discovering devices...") + hid_devices, serial_devices, webusb_available = await discover_devices() + print_devices(hid_devices, serial_devices, webusb_available) + return + + # If no test specified, show help + if not args.test: + parser.print_help() + print("\nUse --list-devices to see available devices") + return + + # Create connection + print(f"Connecting via {args.connection} (device index: {args.device_index})...") + try: + connection, conn_type = await create_connection( + connection_type=args.connection, device_index=args.device_index + ) + print(f"Connected via {conn_type}") + except Exception as e: + print(f"Failed to connect: {e}") + sys.exit(1) + + try: + # Run the selected test + if args.test == "all": + print("\n" + "=" * 70) + print("Running All Tests") + print("=" * 70) + manager_results = await run_all_manager_tests(connection) + btc_results = await run_all_btc_tests(connection) + print("\n" + "=" * 70) + print("All Tests Complete") + print("=" * 70) + + elif args.test == "manager-all": + await run_all_manager_tests(connection) + + elif args.test == "btc-all": + await run_all_btc_tests(connection) + + elif args.test == "manager-device-info": + await test_get_device_info(connection) + + elif args.test == "manager-wallets": + await test_get_wallets(connection) + + elif args.test == "manager-logs": + await test_get_logs(connection) + + elif args.test == "manager-select-wallet": + await test_select_wallet(connection) + + elif args.test == "manager-sdk-version": + await test_get_sdk_version(connection) + + elif args.test == "btc-public-key": + await test_get_public_key(connection, args.btc_path) + + elif args.test == "btc-xpubs": + paths = args.btc_paths if args.btc_paths else None + await test_get_xpubs(connection, paths) + + elif args.test == "btc-sign-txn": + if not args.txn_hex: + print("Error: --txn-hex is required for sign_txn test") + sys.exit(1) + await test_sign_txn(connection, args.txn_hex) + + except KeyboardInterrupt: + print("\n\nInterrupted by user") + except Exception as e: + print(f"\n\nError during test execution: {e}") + import traceback + + traceback.print_exc() + finally: + # Cleanup + try: + await connection.destroy() + print("\nConnection closed") + except Exception: + pass + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/test_app/run.sh b/test_app/run.sh new file mode 100755 index 0000000..3ed4e05 --- /dev/null +++ b/test_app/run.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Helper script to run the test app with proper library paths set + +# Detect Homebrew prefix +if [ -d "/opt/homebrew" ]; then + HOMEBREW_PREFIX="/opt/homebrew" +else + HOMEBREW_PREFIX="/usr/local" +fi + +# Set library paths for macOS +export DYLD_LIBRARY_PATH="$HOMEBREW_PREFIX/lib:$DYLD_LIBRARY_PATH" +export LD_LIBRARY_PATH="$HOMEBREW_PREFIX/lib:$LD_LIBRARY_PATH" + +# Run the test app with all arguments passed through +poetry run python test_app/main.py "$@" diff --git a/test_app/tests_btc.py b/test_app/tests_btc.py new file mode 100644 index 0000000..b6e2d03 --- /dev/null +++ b/test_app/tests_btc.py @@ -0,0 +1,89 @@ +"""Test functions for BtcApp operations.""" +import json +from typing import Any + +from app_btc import BtcApp +from app_btc.operations import GetPublicKeyParams, GetXpubsParams, SignTxnParams +from interfaces import IDeviceConnection + + +async def test_get_public_key(connection: IDeviceConnection, path: str = "m/84'/0'/0'/0/0") -> dict[str, Any]: + """Test getting public key.""" + print("\n=== Testing get_public_key ===") + print(f"Path: {path}") + try: + app = await BtcApp.create(connection) + params = GetPublicKeyParams(path=path) + result = await app.get_public_key(params) + print(f"Public Key: {json.dumps(result.__dict__ if hasattr(result, '__dict__') else str(result), indent=2, default=str)}") + await app.destroy() + return {"success": True, "data": result} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def test_get_xpubs(connection: IDeviceConnection, paths: list[str] = None) -> dict[str, Any]: + """Test getting extended public keys.""" + print("\n=== Testing get_xpubs ===") + if paths is None: + paths = ["m/84'/0'/0'", "m/49'/0'/0'", "m/44'/0'/0'"] + print(f"Paths: {paths}") + try: + app = await BtcApp.create(connection) + params = GetXpubsParams(paths=paths) + result = await app.get_xpubs(params) + print(f"Xpubs: {json.dumps(result.__dict__ if hasattr(result, '__dict__') else str(result), indent=2, default=str)}") + await app.destroy() + return {"success": True, "data": result} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def test_sign_txn(connection: IDeviceConnection, txn_hex: str = None) -> dict[str, Any]: + """Test signing a transaction.""" + print("\n=== Testing sign_txn ===") + print("Note: This requires a valid transaction hex. Using placeholder.") + try: + if not txn_hex: + print("No transaction hex provided. Skipping sign_txn test.") + return {"success": False, "error": "No transaction hex provided"} + + app = await BtcApp.create(connection) + params = SignTxnParams(txn=txn_hex) + result = await app.sign_txn(params) + print(f"Signed Transaction: {json.dumps(result.__dict__ if hasattr(result, '__dict__') else str(result), indent=2, default=str)}") + await app.destroy() + return {"success": True, "data": result} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def run_all_btc_tests(connection: IDeviceConnection) -> dict[str, Any]: + """Run all BtcApp tests.""" + print("\n" + "=" * 50) + print("Running All BtcApp Tests") + print("=" * 50) + + results = {} + + # Test get_public_key with a standard path + results["get_public_key"] = await test_get_public_key(connection, "m/84'/0'/0'/0/0") + + # Test get_xpubs with standard paths + results["get_xpubs"] = await test_get_xpubs(connection) + + # Note: sign_txn requires a valid transaction, so we'll skip it in "all" tests + # results["sign_txn"] = await test_sign_txn(connection, txn_hex) + + print("\n" + "=" * 50) + print("BtcApp Tests Summary") + print("=" * 50) + for test_name, result in results.items(): + status = "✓" if result.get("success") else "✗" + print(f"{status} {test_name}: {result.get('success', False)}") + + return results + diff --git a/test_app/tests_manager.py b/test_app/tests_manager.py new file mode 100644 index 0000000..1e9b58e --- /dev/null +++ b/test_app/tests_manager.py @@ -0,0 +1,103 @@ +"""Test functions for ManagerApp operations.""" + +import json +from typing import Any + +from app_manager import ManagerApp +from interfaces import IDeviceConnection + + +async def test_get_device_info(connection: IDeviceConnection) -> dict[str, Any]: + """Test getting device information.""" + print("\n=== Testing get_device_info ===") + try: + app = await ManagerApp.create(connection) + device_info = await app.get_device_info() + print(f"\n\nDevice Info:\n{device_info}") + return {"success": True, "data": device_info} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def test_get_wallets(connection: IDeviceConnection) -> dict[str, Any]: + """Test getting wallets.""" + print("\n=== Testing get_wallets ===") + try: + app = await ManagerApp.create(connection) + wallets = await app.get_wallets() + print(f"\n\nWallets:\n{wallets}") + return {"success": True, "data": wallets} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def test_get_logs(connection: IDeviceConnection) -> dict[str, Any]: + """Test getting logs.""" + print("\n=== Testing get_logs ===") + try: + app = await ManagerApp.create(connection) + + def on_event(event): + print(f"Log event: {event}") + + logs = await app.get_logs(on_event=on_event) + print(f"Logs:\n{logs}") + return {"success": True, "data": logs} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def test_select_wallet(connection: IDeviceConnection) -> dict[str, Any]: + """Test selecting a wallet.""" + print("\n=== Testing select_wallet ===") + try: + app = await ManagerApp.create(connection) + result = await app.select_wallet() + print(f"\n\nSelect wallet result:\n{type(result.wallet.id)}") + return {"success": True, "data": result} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def test_get_sdk_version(connection: IDeviceConnection) -> dict[str, Any]: + """Test getting SDK version.""" + print("\n=== Testing get_sdk_version ===") + try: + app = await ManagerApp.create(connection) + version = app.get_sdk_version() + print(f"SDK Version: {version}") + is_supported = await app.is_supported() + print(f"Is Supported: {is_supported}") + return {"success": True, "version": version, "is_supported": is_supported} + except Exception as e: + print(f"Error: {e}") + return {"success": False, "error": str(e)} + + +async def run_all_manager_tests(connection: IDeviceConnection) -> dict[str, Any]: + """Run all ManagerApp tests.""" + print("\n" + "=" * 50) + print("Running All ManagerApp Tests") + print("=" * 50) + + results = {} + + results["sdk_version"] = await test_get_sdk_version(connection) + results["device_info"] = await test_get_device_info(connection) + results["wallets"] = await test_get_wallets(connection) + results["logs"] = await test_get_logs(connection) + # Note: select_wallet might require user interaction, so we'll skip it in "all" tests + # results["select_wallet"] = await test_select_wallet(connection) + + print("\n" + "=" * 50) + print("ManagerApp Tests Summary") + print("=" * 50) + for test_name, result in results.items(): + status = "✓" if result.get("success") else "✗" + print(f"{status} {test_name}: {result.get('success', False)}") + + return results