From 2bb5105eff389cde63202bd41bf8ad3ede57ab79 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 15 Mar 2024 16:45:56 -0700 Subject: [PATCH 1/3] Add compages dependency and relock --- pdm.lock | 198 ++++++++++++++++++++++++------------------------- pyproject.toml | 3 +- 2 files changed, 101 insertions(+), 100 deletions(-) diff --git a/pdm.lock b/pdm.lock index 169892e..91a1408 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "compiler", "docs", "lint", "local-provider", "tests"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:c79d285c74c49448b594ca9be98eb21079a8147464da3857832acc7ea45c1f2e" +content_hash = "sha256:04d97e5718881043d2ce53873bdee4f408b415a63e1381d2d763a1c18c23dd73" [[package]] name = "alabaster" @@ -19,11 +19,11 @@ files = [ [[package]] name = "alysis" -version = "0.3.0" +version = "0.4.0" requires_python = ">=3.10" summary = "Ethereum testerchain" dependencies = [ - "compages>=0.2.1", + "compages>=0.3.0", "eth-keys>=0.5", "eth-typing>=4", "eth-utils>=3", @@ -32,8 +32,8 @@ dependencies = [ "setuptools", ] files = [ - {file = "alysis-0.3.0-py3-none-any.whl", hash = "sha256:155bbeca5c52e09ed3d22904cea40115f0862c1f4d884b122a9138829f6e5b9f"}, - {file = "alysis-0.3.0.tar.gz", hash = "sha256:f19cb117fb927247c66b851d9f740f1ba16cd348d3aaf935d6a36b563d7ee2fc"}, + {file = "alysis-0.4.0-py3-none-any.whl", hash = "sha256:b282d5b0d7167041c10c7d9c9c3074e9142afdc4b5389dc51e7f0c194dda1dc4"}, + {file = "alysis-0.4.0.tar.gz", hash = "sha256:5112f6fb60e559c27bdfe32dc3ded3be9581ebeb355b50c72233967ad7a08eeb"}, ] [[package]] @@ -298,97 +298,97 @@ files = [ [[package]] name = "compages" -version = "0.2.1" +version = "0.3.0" requires_python = ">=3.10" summary = "Modular structurer/unstructurer" files = [ - {file = "compages-0.2.1-py3-none-any.whl", hash = "sha256:869c2b1bd015bae8fbaa07cec58a4c3c69c4f547f52548f6ee26f6023745b8b9"}, - {file = "compages-0.2.1.tar.gz", hash = "sha256:0a4df45d3d1ea566a6f7837dbecc46f44eca7c424e210117c76009af5d71ad4f"}, + {file = "compages-0.3.0-py3-none-any.whl", hash = "sha256:8fe1fc9de7ea3b75e966446be68639218a6d9f2320e879eecc65ddb5aec6edf1"}, + {file = "compages-0.3.0.tar.gz", hash = "sha256:3b02fcd9005e70fc796c83e07f6555fecfef24c4cd68194b7d371e49faca50a3"}, ] [[package]] name = "coverage" -version = "7.4.3" +version = "7.4.4" requires_python = ">=3.8" summary = "Code coverage measurement for Python" files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [[package]] name = "coverage" -version = "7.4.3" +version = "7.4.4" extras = ["toml"] requires_python = ">=3.8" summary = "Code coverage measurement for Python" dependencies = [ - "coverage==7.4.3", + "coverage==7.4.4", "tomli; python_full_version <= \"3.11.0a6\"", ] files = [ - {file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"}, - {file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"}, - {file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"}, - {file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"}, - {file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"}, - {file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"}, - {file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"}, - {file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"}, - {file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"}, - {file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"}, - {file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"}, - {file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"}, - {file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"}, - {file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"}, - {file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"}, - {file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"}, - {file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"}, - {file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, ] [[package]] @@ -1232,37 +1232,37 @@ files = [ [[package]] name = "ruff" -version = "0.3.2" +version = "0.3.3" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." files = [ - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:77f2612752e25f730da7421ca5e3147b213dca4f9a0f7e0b534e9562c5441f01"}, - {file = "ruff-0.3.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9966b964b2dd1107797be9ca7195002b874424d1d5472097701ae8f43eadef5d"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b83d17ff166aa0659d1e1deaf9f2f14cbe387293a906de09bc4860717eb2e2da"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb875c6cc87b3703aeda85f01c9aebdce3d217aeaca3c2e52e38077383f7268a"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be75e468a6a86426430373d81c041b7605137a28f7014a72d2fc749e47f572aa"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:967978ac2d4506255e2f52afe70dda023fc602b283e97685c8447d036863a302"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1231eacd4510f73222940727ac927bc5d07667a86b0cbe822024dd00343e77e9"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6d613b19e9a8021be2ee1d0e27710208d1603b56f47203d0abbde906929a9b"}, - {file = "ruff-0.3.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8439338a6303585d27b66b4626cbde89bb3e50fa3cae86ce52c1db7449330a7"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:de8b480d8379620cbb5ea466a9e53bb467d2fb07c7eca54a4aa8576483c35d36"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b74c3de9103bd35df2bb05d8b2899bf2dbe4efda6474ea9681280648ec4d237d"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f380be9fc15a99765c9cf316b40b9da1f6ad2ab9639e551703e581a5e6da6745"}, - {file = "ruff-0.3.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0ac06a3759c3ab9ef86bbeca665d31ad3aa9a4b1c17684aadb7e61c10baa0df4"}, - {file = "ruff-0.3.2-py3-none-win32.whl", hash = "sha256:9bd640a8f7dd07a0b6901fcebccedadeb1a705a50350fb86b4003b805c81385a"}, - {file = "ruff-0.3.2-py3-none-win_amd64.whl", hash = "sha256:0c1bdd9920cab5707c26c8b3bf33a064a4ca7842d91a99ec0634fec68f9f4037"}, - {file = "ruff-0.3.2-py3-none-win_arm64.whl", hash = "sha256:5f65103b1d76e0d600cabd577b04179ff592064eaa451a70a81085930e907d0b"}, - {file = "ruff-0.3.2.tar.gz", hash = "sha256:fa78ec9418eb1ca3db392811df3376b46471ae93792a81af2d1cbb0e5dcb5142"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] name = "setuptools" -version = "69.1.1" +version = "69.2.0" requires_python = ">=3.8" summary = "Easily download, build, install, upgrade, and uninstall Python packages" files = [ - {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"}, - {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [[package]] @@ -1565,10 +1565,10 @@ files = [ [[package]] name = "zipp" -version = "3.17.0" +version = "3.18.1" requires_python = ">=3.8" summary = "Backport of pathlib-compatible object wrapper for zip files" files = [ - {file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"}, - {file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"}, + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, ] diff --git a/pyproject.toml b/pyproject.toml index b3393bf..955998e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "anyio>=3", "setuptools", # required by eth-utils, but it just assumes that it's already installed "trio-typing>=0.9.0", + "compages>=0.3", ] requires-python = ">=3.10" license = "MIT" @@ -26,7 +27,7 @@ compiler = [ "py-solc-x>=2", ] local-provider = [ - "alysis>=0.3.0", + "alysis>=0.4.0", "starlette", "hypercorn", ] From d9f64d2c8c1c591e9470a60fd33b23971e77acad Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Mon, 11 Mar 2024 13:50:13 -0700 Subject: [PATCH 2/3] Switch to `compages` for JSON serializatioon --- docs/changelog.rst | 2 + pons/__init__.py | 13 +- pons/_client.py | 471 ++++++++++++++++---------------- pons/_entities.py | 339 +++++++++-------------- pons/_fallback_provider.py | 3 +- pons/_http_provider_server.py | 6 +- pons/_local_provider.py | 6 +- pons/_provider.py | 135 +-------- pons/_serialization.py | 177 ++++++++++++ pyproject.toml | 2 + tests/test_client.py | 44 +-- tests/test_entities.py | 236 +--------------- tests/test_fallback_provider.py | 3 +- tests/test_local_provider.py | 175 +----------- tests/test_provider.py | 77 ++---- tests/test_serialization.py | 38 +++ 16 files changed, 663 insertions(+), 1064 deletions(-) create mode 100644 pons/_serialization.py create mode 100644 tests/test_serialization.py diff --git a/docs/changelog.rst b/docs/changelog.rst index 7387d8e..614ebb9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -15,6 +15,7 @@ Changed - ``ClientSession.estimate_transact()`` and ``estimate_deploy()`` now require a ``sender_address`` parameter. (PR_62_) - Switched to ``alysis`` from ``eth-tester`` for the backend of ``LocalProvider``. (PR_70_) - Bumped the minimum Python version to 3.10. (PR_72_) +- The entities are now dataclasses instead of namedtuples. (PR_75_) Added @@ -65,6 +66,7 @@ Fixed .. _PR_68: https://github.com/fjarri/pons/pull/68 .. _PR_70: https://github.com/fjarri/pons/pull/70 .. _PR_72: https://github.com/fjarri/pons/pull/72 +.. _PR_75: https://github.com/fjarri/pons/pull/75 0.7.0 (09-07-2023) diff --git a/pons/__init__.py b/pons/__init__.py index 0726226..95f5cf3 100644 --- a/pons/__init__.py +++ b/pons/__init__.py @@ -38,7 +38,7 @@ Mutability, Receive, ) -from ._entities import Address, Amount, Block, BlockHash, TxHash +from ._entities import Address, Amount, Block, BlockHash, RPCError, RPCErrorCode, TxHash from ._fallback_provider import ( CycleFallback, FallbackProvider, @@ -48,15 +48,8 @@ ) from ._http_provider_server import HTTPProviderServer from ._local_provider import LocalProvider, SnapshotID -from ._provider import ( - JSON, - HTTPProvider, - ProtocolError, - Provider, - RPCError, - RPCErrorCode, - Unreachable, -) +from ._provider import HTTPProvider, ProtocolError, Provider, Unreachable +from ._serialization import JSON from ._signer import AccountSigner, Signer __all__ = [ diff --git a/pons/_client.py b/pons/_client.py index ffa850c..7fdbbde 100644 --- a/pons/_client.py +++ b/pons/_client.py @@ -1,7 +1,6 @@ -from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Mapping, Sequence -from contextlib import asynccontextmanager +from collections.abc import AsyncIterator, Iterable, Iterator, Sequence +from contextlib import asynccontextmanager, contextmanager from enum import Enum -from functools import wraps from typing import Any, ParamSpec, TypeVar, cast import anyio @@ -29,30 +28,23 @@ BlockFilterId, BlockHash, BlockInfo, + EstimateGasParams, + EthCallParams, + FilterParams, LogEntry, LogFilter, LogFilterId, PendingTransactionFilter, PendingTransactionFilterId, - RPCDecodingError, + RPCError, + RPCErrorCode, TxHash, TxInfo, TxReceipt, - rpc_decode_data, - rpc_decode_quantity, - rpc_encode_block, - rpc_encode_data, - rpc_encode_quantity, -) -from ._provider import ( - JSON, - InvalidResponse, - Provider, - ProviderSession, - ResponseDict, - RPCError, - RPCErrorCode, + Type2Transaction, ) +from ._provider import InvalidResponse, Provider, ProviderSession +from ._serialization import JSON, StructuringError, structure, unstructure from ._signer import Signer @@ -109,9 +101,8 @@ class ProviderError(RemoteError): @classmethod def from_rpc_error(cls, exc: RPCError) -> "ProviderError": - data = rpc_decode_data(exc.data) if exc.data else None parsed_code = RPCErrorCode.from_int(exc.code) - return cls(exc.code, parsed_code, exc.message, data) + return cls(exc.code, parsed_code, exc.message, exc.data) def __init__(self, raw_code: int, code: RPCErrorCode, message: str, data: None | bytes = None): super().__init__(raw_code, code, message, data) @@ -132,25 +123,49 @@ def __str__(self) -> str: RetType = TypeVar("RetType") -def rpc_call( - method_name: str, -) -> Callable[[Callable[Param, Awaitable[RetType]]], Callable[Param, Awaitable[RetType]]]: +@contextmanager +def convert_errors(method_name: str) -> Iterator[None]: + try: + yield + except (StructuringError, InvalidResponse) as exc: + raise BadResponseFormat(f"{method_name}: {exc}") from exc + except RPCError as exc: + raise ProviderError.from_rpc_error(exc) from exc + + +async def rpc_call( + provider_session: ProviderSession, method_name: str, ret_type: type[RetType], *args: Any +) -> RetType: """Catches various response formatting errors and returns them in a unified way.""" + with convert_errors(method_name): + result = await provider_session.rpc(method_name, *(unstructure(arg) for arg in args)) + return structure(ret_type, result) - def _wrapper(func: Callable[Param, Awaitable[RetType]]) -> Callable[Param, Awaitable[RetType]]: - @wraps(func) - async def _wrapped(*args: Any, **kwargs: Any) -> RetType: - try: - result = await func(*args, **kwargs) - except (RPCDecodingError, InvalidResponse) as exc: - raise BadResponseFormat(f"{method_name}: {exc}") from exc - except RPCError as exc: - raise ProviderError.from_rpc_error(exc) from exc - return result - return _wrapped +async def rpc_call_pin( + provider_session: ProviderSession, method_name: str, ret_type: type[RetType], *args: Any +) -> tuple[RetType, tuple[int, ...]]: + """Catches various response formatting errors and returns them in a unified way.""" + with convert_errors(method_name): + result, provider_path = await provider_session.rpc_and_pin( + method_name, *(unstructure(arg) for arg in args) + ) + return structure(ret_type, result), provider_path - return _wrapper + +async def rpc_call_at_pin( + provider_session: ProviderSession, + provider_path: tuple[int, ...], + method_name: str, + ret_type: type[RetType], + *args: Any, +) -> RetType: + """Catches various response formatting errors and returns them in a unified way.""" + with convert_errors(method_name): + result = await provider_session.rpc_at_pin( + provider_path, method_name, *(unstructure(arg) for arg in args) + ) + return structure(ret_type, result) class ContractPanicReason(Enum): @@ -275,82 +290,78 @@ def __init__(self, provider_session: ProviderSession): self._net_version: None | str = None self._chain_id: None | int = None - @rpc_call("net_version") async def net_version(self) -> str: """Calls the ``net_version`` RPC method.""" if self._net_version is None: - result = await self._provider_session.rpc("net_version") - if not isinstance(result, str): - raise RPCDecodingError("expected a string result") - self._net_version = result + self._net_version = await rpc_call(self._provider_session, "net_version", str) return self._net_version - @rpc_call("eth_chainId") async def eth_chain_id(self) -> int: """Calls the ``eth_chainId`` RPC method.""" if self._chain_id is None: - result = await self._provider_session.rpc("eth_chainId") - self._chain_id = rpc_decode_quantity(result) + self._chain_id = await rpc_call(self._provider_session, "eth_chainId", int) return self._chain_id - @rpc_call("eth_getBalance") async def eth_get_balance(self, address: Address, block: int | Block = Block.LATEST) -> Amount: """Calls the ``eth_getBalance`` RPC method.""" - result = await self._provider_session.rpc( - "eth_getBalance", address.rpc_encode(), rpc_encode_block(block) - ) - return Amount.rpc_decode(result) + return await rpc_call(self._provider_session, "eth_getBalance", Amount, address, block) - @rpc_call("eth_getTransactionByHash") async def eth_get_transaction_by_hash(self, tx_hash: TxHash) -> None | TxInfo: """Calls the ``eth_getTransactionByHash`` RPC method.""" - result = await self._provider_session.rpc_dict( - "eth_getTransactionByHash", tx_hash.rpc_encode() + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + None | TxInfo, + await rpc_call( + self._provider_session, + "eth_getTransactionByHash", + None | TxInfo, # type: ignore[arg-type] + tx_hash, + ), ) - if not result: - return None - return TxInfo.rpc_decode(result) - @rpc_call("eth_getTransactionReceipt") async def eth_get_transaction_receipt(self, tx_hash: TxHash) -> None | TxReceipt: """Calls the ``eth_getTransactionReceipt`` RPC method.""" - result = await self._provider_session.rpc_dict( - "eth_getTransactionReceipt", tx_hash.rpc_encode() + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + None | TxReceipt, + await rpc_call( + self._provider_session, + "eth_getTransactionReceipt", + None | TxReceipt, # type: ignore[arg-type] + tx_hash, + ), ) - if not result: - return None - return TxReceipt.rpc_decode(result) - @rpc_call("eth_getTransactionCount") async def eth_get_transaction_count( self, address: Address, block: int | Block = Block.LATEST ) -> int: """Calls the ``eth_getTransactionCount`` RPC method.""" - result = await self._provider_session.rpc( - "eth_getTransactionCount", address.rpc_encode(), rpc_encode_block(block) + return await rpc_call( + self._provider_session, + "eth_getTransactionCount", + int, + address, + block, ) - return rpc_decode_quantity(result) - @rpc_call("eth_getCode") async def eth_get_code(self, address: Address, block: int | Block = Block.LATEST) -> bytes: """Calls the ``eth_getCode`` RPC method.""" - result = await self._provider_session.rpc( - "eth_getCode", address.rpc_encode(), rpc_encode_block(block) - ) - return rpc_decode_data(result) + return await rpc_call(self._provider_session, "eth_getCode", bytes, address, block) - @rpc_call("eth_getStorageAt") async def eth_get_storage_at( self, address: Address, position: int, block: int | Block = Block.LATEST ) -> bytes: """Calls the ``eth_getCode`` RPC method.""" - result = await self._provider_session.rpc( + return await rpc_call( + self._provider_session, "eth_getStorageAt", - address.rpc_encode(), - rpc_encode_quantity(position), - rpc_encode_block(block), + bytes, + address, + position, + block, ) - return rpc_decode_data(result) async def wait_for_transaction_receipt( self, tx_hash: TxHash, poll_latency: float = 1.0 @@ -362,7 +373,6 @@ async def wait_for_transaction_receipt( return receipt await anyio.sleep(poll_latency) - @rpc_call("eth_call") async def eth_call( self, call: BoundMethodCall, @@ -376,29 +386,23 @@ async def eth_call( If ``sender_address`` is provided, it will be included in the call and affect the return value if the method uses ``msg.sender`` internally. """ - tx = { - "to": call.contract_address.rpc_encode(), - "data": rpc_encode_data(call.data_bytes), - } - if sender_address is not None: - tx["from"] = sender_address.rpc_encode() - result = await self._provider_session.rpc("eth_call", tx, rpc_encode_block(block)) - - encoded_output = rpc_decode_data(result) + params = EthCallParams(to=call.contract_address, data=call.data_bytes, from_=sender_address) + + encoded_output = await rpc_call( + self._provider_session, + "eth_call", + bytes, + params, + block, + ) return call.decode_output(encoded_output) - @rpc_call("eth_sendRawTransaction") async def _eth_send_raw_transaction(self, tx_bytes: bytes) -> TxHash: """Sends a signed and serialized transaction.""" - result = await self._provider_session.rpc( - "eth_sendRawTransaction", rpc_encode_data(tx_bytes) - ) - return TxHash.rpc_decode(result) + return await rpc_call(self._provider_session, "eth_sendRawTransaction", TxHash, tx_bytes) - @rpc_call("eth_estimateGas") - async def _estimate_gas(self, tx: Mapping[str, JSON], block: int | Block) -> int: - result = await self._provider_session.rpc("eth_estimateGas", tx, rpc_encode_block(block)) - return rpc_decode_quantity(result) + async def _estimate_gas(self, params: EstimateGasParams, block: int | Block) -> int: + return await rpc_call(self._provider_session, "eth_estimateGas", int, params, block) async def estimate_deploy( self, @@ -416,16 +420,11 @@ async def estimate_deploy( or :py:class:`ContractError` if a known error was caught during the dry run. If the error was unknown, falls back to :py:class:`ProviderError`. """ - if amount is None: - amount = Amount(0) - - tx = { - "from": sender_address.rpc_encode(), - "data": rpc_encode_data(call.data_bytes), - "value": amount.rpc_encode(), - } + params = EstimateGasParams( + from_=sender_address, data=call.data_bytes, value=amount or Amount(0) + ) try: - return await self._estimate_gas(tx, block) + return await self._estimate_gas(params, block) except ProviderError as exc: raise decode_contract_error(call.contract_abi, exc) from exc @@ -442,12 +441,8 @@ async def estimate_transfer( """ # source_address and amount are optional, # but if they are specified, we will fail here instead of later. - tx = { - "from": source_address.rpc_encode(), - "to": destination_address.rpc_encode(), - "value": amount.rpc_encode(), - } - return await self._estimate_gas(tx, block) + params = EstimateGasParams(from_=source_address, to=destination_address, value=amount) + return await self._estimate_gas(params, block) async def estimate_transact( self, @@ -466,55 +461,58 @@ async def estimate_transact( or :py:class:`ContractError` if a known error was caught during the dry run. If the error was unknown, falls back to :py:class:`ProviderError`. """ - if amount is None: - amount = Amount(0) - - tx = { - "from": sender_address.rpc_encode(), - "to": call.contract_address.rpc_encode(), - "data": rpc_encode_data(call.data_bytes), - "value": amount.rpc_encode(), - } + params = EstimateGasParams( + from_=sender_address, + to=call.contract_address, + data=call.data_bytes, + value=amount or Amount(0), + ) try: - return await self._estimate_gas(tx, block) + return await self._estimate_gas(params, block) except ProviderError as exc: raise decode_contract_error(call.contract_abi, exc) from exc - @rpc_call("eth_gasPrice") async def eth_gas_price(self) -> Amount: """Calls the ``eth_gasPrice`` RPC method.""" - result = await self._provider_session.rpc("eth_gasPrice") - return Amount.rpc_decode(result) + return await rpc_call(self._provider_session, "eth_gasPrice", Amount) - @rpc_call("eth_blockNumber") async def eth_block_number(self) -> int: """Calls the ``eth_blockNumber`` RPC method.""" - result = await self._provider_session.rpc("eth_blockNumber") - return rpc_decode_quantity(result) + return await rpc_call(self._provider_session, "eth_blockNumber", int) - @rpc_call("eth_getBlockByHash") async def eth_get_block_by_hash( self, block_hash: BlockHash, *, with_transactions: bool = False ) -> None | BlockInfo: """Calls the ``eth_getBlockByHash`` RPC method.""" - result = await self._provider_session.rpc_dict( - "eth_getBlockByHash", block_hash.rpc_encode(), with_transactions + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + None | BlockInfo, + await rpc_call( + self._provider_session, + "eth_getBlockByHash", + None | BlockInfo, # type: ignore[arg-type] + block_hash, + with_transactions, + ), ) - if result is None: - return None - return BlockInfo.rpc_decode(result) - @rpc_call("eth_getBlockByNumber") async def eth_get_block_by_number( self, block: int | Block = Block.LATEST, *, with_transactions: bool = False ) -> None | BlockInfo: """Calls the ``eth_getBlockByNumber`` RPC method.""" - result = await self._provider_session.rpc_dict( - "eth_getBlockByNumber", rpc_encode_block(block), with_transactions + # Need an explicit cast, mypy doesn't work with union types correctly. + # See https://github.com/python/mypy/issues/16935 + return cast( + None | BlockInfo, + await rpc_call( + self._provider_session, + "eth_getBlockByNumber", + None | BlockInfo, # type: ignore[arg-type] + block, + with_transactions, + ), ) - if result is None: - return None - return BlockInfo.rpc_decode(result) async def broadcast_transfer( self, @@ -535,16 +533,20 @@ async def broadcast_transfer( max_gas_price = await self.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) nonce = await self.eth_get_transaction_count(signer.address, Block.PENDING) - tx: dict[str, str] = { - "type": "0x2", # EIP-2930 transaction - "chainId": rpc_encode_quantity(chain_id), - "to": destination_address.rpc_encode(), - "value": amount.rpc_encode(), - "gas": rpc_encode_quantity(gas), - "maxFeePerGas": max_gas_price.rpc_encode(), - "maxPriorityFeePerGas": max_tip.rpc_encode(), - "nonce": rpc_encode_quantity(nonce), - } + tx = cast( + dict[str, JSON], + unstructure( + Type2Transaction( + chain_id=chain_id, + to=destination_address, + value=amount, + gas=gas, + max_fee_per_gas=max_gas_price, + max_priority_fee_per_gas=max_tip, + nonce=nonce, + ) + ), + ) signed_tx = signer.sign_transaction(tx) return await self._eth_send_raw_transaction(signed_tx) @@ -601,16 +603,20 @@ async def deploy( max_gas_price = await self.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) nonce = await self.eth_get_transaction_count(signer.address, Block.PENDING) - tx: dict[str, str] = { - "type": "0x2", # EIP-2930 transaction - "chainId": rpc_encode_quantity(chain_id), - "value": amount.rpc_encode(), - "gas": rpc_encode_quantity(gas), - "maxFeePerGas": max_gas_price.rpc_encode(), - "maxPriorityFeePerGas": max_tip.rpc_encode(), - "nonce": rpc_encode_quantity(nonce), - "data": rpc_encode_data(call.data_bytes), - } + tx = cast( + dict[str, JSON], + unstructure( + Type2Transaction( + chain_id=chain_id, + value=amount, + gas=gas, + max_fee_per_gas=max_gas_price, + max_priority_fee_per_gas=max_tip, + nonce=nonce, + data=call.data_bytes, + ) + ), + ) signed_tx = signer.sign_transaction(tx) tx_hash = await self._eth_send_raw_transaction(signed_tx) receipt = await self.wait_for_transaction_receipt(tx_hash) @@ -655,17 +661,21 @@ async def broadcast_transact( max_gas_price = await self.eth_gas_price() max_tip = min(Amount.gwei(1), max_gas_price) nonce = await self.eth_get_transaction_count(signer.address, Block.PENDING) - tx: dict[str, str] = { - "type": "0x2", # EIP-2930 transaction - "chainId": rpc_encode_quantity(chain_id), - "to": call.contract_address.rpc_encode(), - "value": amount.rpc_encode(), - "gas": rpc_encode_quantity(gas), - "maxFeePerGas": max_gas_price.rpc_encode(), - "maxPriorityFeePerGas": max_tip.rpc_encode(), - "nonce": rpc_encode_quantity(nonce), - "data": rpc_encode_data(call.data_bytes), - } + tx = cast( + dict[str, JSON], + unstructure( + Type2Transaction( + chain_id=chain_id, + to=call.contract_address, + value=amount, + gas=gas, + max_fee_per_gas=max_gas_price, + max_priority_fee_per_gas=max_tip, + nonce=nonce, + data=call.data_bytes, + ) + ), + ) signed_tx = signer.sign_transaction(tx) return await self._eth_send_raw_transaction(signed_tx) @@ -713,10 +723,6 @@ async def transact( ) event_results = [] for log_entry in log_entries: - # We can't ensure it statically, since `eth_getFilterChanges` return type depends - # on the filter passed to it. - log_entry = cast(LogEntry, log_entry) - if log_entry.transaction_hash != receipt.transaction_hash: continue @@ -727,32 +733,6 @@ async def transact( return results - def _encode_filter_params( - self, - source: None | Address | Iterable[Address], - event_filter: None | EventFilter, - from_block: int | Block, - to_block: int | Block, - ) -> JSON: - params: dict[str, Any] = { - "fromBlock": rpc_encode_block(from_block), - "toBlock": rpc_encode_block(to_block), - } - if isinstance(source, Address): - params["address"] = source.rpc_encode() - elif source: - params["address"] = [address.rpc_encode() for address in source] - if event_filter: - encoded_topics: list[None | list[str]] = [] - for topic in event_filter.topics: - if topic is None: - encoded_topics.append(None) - else: - encoded_topics.append([elem.rpc_encode() for elem in topic]) - params["topics"] = encoded_topics - return params - - @rpc_call("eth_getLogs") async def eth_get_logs( self, source: None | Address | Iterable[Address] = None, @@ -761,32 +741,30 @@ async def eth_get_logs( to_block: int | Block = Block.LATEST, ) -> tuple[LogEntry, ...]: """Calls the ``eth_getLogs`` RPC method.""" - params = self._encode_filter_params( - source=source, event_filter=event_filter, from_block=from_block, to_block=to_block + if isinstance(source, Iterable): + source = tuple(source) + params = FilterParams( + from_block=from_block, + to_block=to_block, + address=source, + topics=event_filter.topics if event_filter is not None else None, ) - result = await self._provider_session.rpc("eth_getLogs", params) - # TODO: this will go away with generalized RPC decoding. - if not isinstance(result, list): - raise InvalidResponse(f"Expected a list as a response, got {type(result).__name__}") - return tuple(LogEntry.rpc_decode(ResponseDict(elem)) for elem in result) + return await rpc_call(self._provider_session, "eth_getLogs", tuple[LogEntry, ...], params) - @rpc_call("eth_newBlockFilter") async def eth_new_block_filter(self) -> BlockFilter: """Calls the ``eth_newBlockFilter`` RPC method.""" - result, provider_path = await self._provider_session.rpc_and_pin("eth_newBlockFilter") - filter_id = BlockFilterId.rpc_decode(result) - return BlockFilter(id_=filter_id, provider_path=provider_path) + result, provider_path = await rpc_call_pin( + self._provider_session, "eth_newBlockFilter", BlockFilterId + ) + return BlockFilter(id_=result, provider_path=provider_path) - @rpc_call("eth_newPendingTransactionFilter") async def eth_new_pending_transaction_filter(self) -> PendingTransactionFilter: """Calls the ``eth_newPendingTransactionFilter`` RPC method.""" - result, provider_path = await self._provider_session.rpc_and_pin( - "eth_newPendingTransactionFilter" + result, provider_path = await rpc_call_pin( + self._provider_session, "eth_newPendingTransactionFilter", PendingTransactionFilterId ) - filter_id = PendingTransactionFilterId.rpc_decode(result) - return PendingTransactionFilter(id_=filter_id, provider_path=provider_path) + return PendingTransactionFilter(id_=result, provider_path=provider_path) - @rpc_call("eth_newFilter") async def eth_new_filter( self, source: None | Address | Iterable[Address] = None, @@ -795,39 +773,52 @@ async def eth_new_filter( to_block: int | Block = Block.LATEST, ) -> LogFilter: """Calls the ``eth_newFilter`` RPC method.""" - params = self._encode_filter_params( - source=source, event_filter=event_filter, from_block=from_block, to_block=to_block + if isinstance(source, Iterable): + source = tuple(source) + params = FilterParams( + from_block=from_block, + to_block=to_block, + address=source, + topics=event_filter.topics if event_filter is not None else None, ) - result, provider_path = await self._provider_session.rpc_and_pin("eth_newFilter", params) - filter_id = LogFilterId.rpc_decode(result) - return LogFilter(id_=filter_id, provider_path=provider_path) + result, provider_path = await rpc_call_pin( + self._provider_session, "eth_newFilter", LogFilterId, params + ) + return LogFilter(id_=result, provider_path=provider_path) - def _parse_filter_result( - self, - filter_: BlockFilter | PendingTransactionFilter | LogFilter, - result: JSON, + async def _query_filter( + self, method_name: str, filter_: BlockFilter | PendingTransactionFilter | LogFilter ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: - # TODO: this will go away with generalized RPC decoding. - if not isinstance(result, list): - raise InvalidResponse(f"Expected a list as a response, got {type(result).__name__}") - if isinstance(filter_, BlockFilter): - return tuple(BlockHash.rpc_decode(elem) for elem in result) + return await rpc_call_at_pin( + self._provider_session, + filter_.provider_path, + method_name, + tuple[BlockHash, ...], + filter_.id_, + ) if isinstance(filter_, PendingTransactionFilter): - return tuple(TxHash.rpc_decode(elem) for elem in result) - return tuple(LogEntry.rpc_decode(ResponseDict(elem)) for elem in result) + return await rpc_call_at_pin( + self._provider_session, + filter_.provider_path, + method_name, + tuple[TxHash, ...], + filter_.id_, + ) + return await rpc_call_at_pin( + self._provider_session, + filter_.provider_path, + method_name, + tuple[LogEntry, ...], + filter_.id_, + ) - @rpc_call("eth_getFilterLogs") async def eth_get_filter_logs( self, filter_: BlockFilter | PendingTransactionFilter | LogFilter ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: """Calls the ``eth_getFilterLogs`` RPC method.""" - result = await self._provider_session.rpc_at_pin( - filter_.provider_path, "eth_getFilterLogs", filter_.id_.rpc_encode() - ) - return self._parse_filter_result(filter_, result) + return await self._query_filter("eth_getFilterLogs", filter_) - @rpc_call("eth_getFilterChanges") async def eth_get_filter_changes( self, filter_: BlockFilter | PendingTransactionFilter | LogFilter ) -> tuple[BlockHash, ...] | tuple[TxHash, ...] | tuple[LogEntry, ...]: @@ -835,11 +826,7 @@ async def eth_get_filter_changes( Calls the ``eth_getFilterChanges`` RPC method. Depending on what ``filter_`` was, returns a tuple of corresponding results. """ - # TODO: split into separate functions with specific return types? - result = await self._provider_session.rpc_at_pin( - filter_.provider_path, "eth_getFilterChanges", filter_.id_.rpc_encode() - ) - return self._parse_filter_result(filter_, result) + return await self._query_filter("eth_getFilterChanges", filter_) async def iter_blocks(self, poll_interval: int = 1) -> AsyncIterator[BlockHash]: """Yields hashes of new blocks being mined.""" diff --git a/pons/_entities.py b/pons/_entities.py index e94259f..5eed47e 100644 --- a/pons/_entities.py +++ b/pons/_entities.py @@ -1,18 +1,11 @@ from abc import ABC, abstractmethod -from collections.abc import Iterable, Sequence +from dataclasses import dataclass from enum import Enum from functools import cached_property -from typing import Any, NamedTuple, TypeVar, cast +from typing import Any, NewType, TypeVar, cast from eth_utils import to_canonical_address, to_checksum_address -from ._provider import ResponseDict - - -class RPCDecodingError(Exception): - """Raised on an error when decoding a value in an RPC response.""" - - TypedDataLike = TypeVar("TypedDataLike", bound="TypedData") @@ -35,16 +28,6 @@ def __init__(self, value: bytes): def _length(self) -> int: """Returns the length of this type's values representation in bytes.""" - def rpc_encode(self) -> str: - return rpc_encode_data(self._value) - - @classmethod - def rpc_decode(cls: type[TypedDataLike], val: Any) -> TypedDataLike: - try: - return cls(rpc_decode_data(val)) - except ValueError as exc: - raise RPCDecodingError(str(exc)) from exc - def __bytes__(self) -> bytes: return self._value @@ -73,19 +56,12 @@ def __init__(self, value: int): raise ValueError(f"{self.__class__.__name__} must be non-negative, got {value}") self._value = value - def rpc_encode(self) -> str: - return rpc_encode_quantity(self._value) - - @classmethod - def rpc_decode(cls: type[TypedQuantityLike], val: Any) -> TypedQuantityLike: - # `rpc_decode_quantity` will raise RPCDecodingError on any error, - # and if it succeeds, constructor won't raise anything - - # the value is already guaranteed to be `int` and non-negative - return cls(rpc_decode_quantity(val)) - def __hash__(self) -> int: return hash(self._value) + def __int__(self) -> int: + return self._value + def _check_type(self: TypedQuantityLike, other: Any) -> TypedQuantityLike: if type(self) != type(other): raise TypeError(f"Incompatible types: {type(self).__name__} and {type(other).__name__}") @@ -196,11 +172,6 @@ def checksum(self) -> str: """Retunrs the checksummed hex representation of the address.""" return to_checksum_address(self._value) - def rpc_encode(self) -> str: - # Overriding the base class method to encode into a checksummed address - - # some providers require it. - return self.checksum - def __str__(self) -> str: return self.checksum @@ -239,17 +210,20 @@ class LogFilterId(TypedQuantity): """A log filter identifier (returned by ``eth_newFilter``).""" -class BlockFilter(NamedTuple): +@dataclass +class BlockFilter: id_: BlockFilterId provider_path: tuple[int, ...] -class PendingTransactionFilter(NamedTuple): +@dataclass +class PendingTransactionFilter: id_: PendingTransactionFilterId provider_path: tuple[int, ...] -class LogFilter(NamedTuple): +@dataclass +class LogFilter: id_: LogFilterId provider_path: tuple[int, ...] @@ -275,7 +249,8 @@ def _length(self) -> int: return 32 -class TxInfo(NamedTuple): +@dataclass +class TxInfo: """Transaction info.""" # TODO: make an enum? @@ -327,35 +302,9 @@ class TxInfo(NamedTuple): max_priority_fee_per_gas: None | Amount """``maxPriorityFeePerGas`` value specified by the sender. Only for EIP1559 transactions.""" - @classmethod - def rpc_decode(cls, val: ResponseDict) -> "TxInfo": - max_fee_per_gas = Amount.rpc_decode(val["maxFeePerGas"]) if "maxFeePerGas" in val else None - max_priority_fee_per_gas = ( - Amount.rpc_decode(val["maxPriorityFeePerGas"]) - if "maxPriorityFeePerGas" in val - else None - ) - return cls( - type_=rpc_decode_quantity(val["type"]), - hash_=TxHash.rpc_decode(val["hash"]), - input_=rpc_decode_data(val["input"]) or None, - block_hash=BlockHash.rpc_decode(val["blockHash"]) if val["blockHash"] else None, - block_number=rpc_decode_quantity(val["blockNumber"]), - transaction_index=( - rpc_decode_quantity(val["transactionIndex"]) if val["transactionIndex"] else None - ), - from_=Address.rpc_decode(val["from"]), - to=Address.rpc_decode(val["to"]) if val["to"] else None, - value=Amount.rpc_decode(val["value"]), - nonce=rpc_decode_quantity(val["nonce"]), - max_fee_per_gas=max_fee_per_gas, - max_priority_fee_per_gas=max_priority_fee_per_gas, - gas=rpc_decode_quantity(val["gas"]), - gas_price=Amount.rpc_decode(val["gasPrice"]), - ) - - -class BlockInfo(NamedTuple): + +@dataclass +class BlockInfo: """Block info.""" number: int @@ -394,57 +343,15 @@ class BlockInfo(NamedTuple): timestamp: int """Block's timestamp.""" - transaction_hashes: tuple[TxHash, ...] - """A list of transaction hashes in this block.""" - - transactions: None | tuple[TxInfo, ...] + transactions: tuple[TxInfo, ...] | tuple[TxHash, ...] """ - A list of details of transactions in this block. - Only present if it was requested. + A list of transaction hashes in this block, or a list of details of transactions in this block, + depending on what was requested. """ - @classmethod - def rpc_decode(cls, val: ResponseDict) -> "BlockInfo": - transactions: None | tuple[TxInfo, ...] - transaction_hashes: tuple[TxHash, ...] - transactions_raw = val["transactions"] - if not isinstance(transactions_raw, Sequence): - raise RPCDecodingError( - f"`transactions` in a block info must be an iterable, got {transactions_raw}" - ) - if len(transactions_raw) == 0: - transactions = () - transaction_hashes = () - elif isinstance(transactions_raw[0], str): - transactions = None - transaction_hashes = tuple(TxHash.rpc_decode(tx_hash) for tx_hash in transactions_raw) - else: - transactions = tuple( - TxInfo.rpc_decode(ResponseDict(tx_info)) for tx_info in transactions_raw - ) - transaction_hashes = tuple(tx.hash_ for tx in transactions) - - return cls( - number=rpc_decode_quantity(val["number"]), - hash_=BlockHash.rpc_decode(val["hash"]) if val["hash"] else None, - parent_hash=BlockHash.rpc_decode(val["parentHash"]), - nonce=rpc_decode_quantity(val["nonce"]) if val["nonce"] is not None else None, - difficulty=rpc_decode_quantity(val["difficulty"]), - total_difficulty=rpc_decode_quantity(val["totalDifficulty"]) - if val["totalDifficulty"] is not None - else None, - size=rpc_decode_quantity(val["size"]), - gas_limit=rpc_decode_quantity(val["gasLimit"]), - gas_used=rpc_decode_quantity(val["gasUsed"]), - base_fee_per_gas=Amount.rpc_decode(val["baseFeePerGas"]), - timestamp=rpc_decode_quantity(val["timestamp"]), - miner=Address.rpc_decode(val["miner"]) if val["miner"] else None, - transactions=transactions, - transaction_hashes=transaction_hashes, - ) - - -class LogEntry(NamedTuple): + +@dataclass +class LogEntry: """Log entry metadata.""" removed: bool @@ -486,26 +393,9 @@ class LogEntry(NamedTuple): block_number: int """The block number where this log was.""" - @classmethod - def rpc_decode(cls, val: ResponseDict) -> "LogEntry": - topics = val["topics"] - if not isinstance(topics, Iterable): - raise RPCDecodingError(f"`topics` in a log entry must be an iterable, got {topics}") - - return cls( - removed=rpc_decode_bool(val["removed"]), - log_index=rpc_decode_quantity(val["logIndex"]), - transaction_index=rpc_decode_quantity(val["transactionIndex"]), - transaction_hash=TxHash.rpc_decode(val["transactionHash"]), - block_hash=BlockHash.rpc_decode(val["blockHash"]), - block_number=rpc_decode_quantity(val["blockNumber"]), - address=Address.rpc_decode(val["address"]), - data=rpc_decode_data(val["data"]), - topics=tuple(LogTopic.rpc_decode(topic) for topic in topics), - ) - - -class TxReceipt(NamedTuple): + +@dataclass +class TxReceipt: """Transaction receipt.""" block_hash: BlockHash @@ -548,84 +438,111 @@ class TxReceipt(NamedTuple): type_: int """Transaction type: 0 for legacy transactions, 2 for EIP1559 transactions.""" - succeeded: bool - """Whether the transaction was successful.""" + status: int + """1 if the transaction was successful, 0 otherwise.""" logs: tuple[LogEntry, ...] """An array of log objects generated by this transaction.""" + @property + def succeeded(self) -> bool: + """``True`` if the transaction succeeded.""" + return self.status == 1 + + +class RPCErrorCode(Enum): + """Known RPC error codes returned by providers.""" + + # This is our placeholder value, shouldn't be encountered in a remote server response + UNKNOWN_REASON = 0 + """An error code whose description is not present in this enum.""" + + SERVER_ERROR = -32000 + """Reserved for implementation-defined server-errors. See the message for details.""" + + INVALID_REQUEST = -32600 + """The JSON sent is not a valid Request object.""" + + METHOD_NOT_FOUND = -32601 + """The method does not exist / is not available.""" + + INVALID_PARAMETER = -32602 + """Invalid method parameter(s).""" + + EXECUTION_ERROR = 3 + """Contract transaction failed during execution. See the data for details.""" + + @classmethod + def from_int(cls, val: int) -> "RPCErrorCode": + try: + return cls(val) + except ValueError: + return cls.UNKNOWN_REASON + + +# Need a newtype because unlike all other integers, this one is not hexified on serialization. +ErrorCode = NewType("ErrorCode", int) + + +@dataclass +class RPCError(Exception): + """A wrapper for a call execution error returned as a proper RPC response.""" + + # Taking an integer and not `RPCErrorCode` here + # since the codes may differ between providers. + code: ErrorCode + message: str + data: None | bytes = None + @classmethod - def rpc_decode(cls, val: ResponseDict) -> "TxReceipt": - contract_address = val["contractAddress"] - logs = val["logs"] - if not isinstance(logs, Iterable): - raise RPCDecodingError(f"`logs` in a tx receipt must be an iterable, got {logs}") - - return cls( - block_hash=BlockHash.rpc_decode(val["blockHash"]), - block_number=rpc_decode_quantity(val["blockNumber"]), - contract_address=Address.rpc_decode(contract_address) if contract_address else None, - cumulative_gas_used=rpc_decode_quantity(val["cumulativeGasUsed"]), - effective_gas_price=Amount.rpc_decode(val["effectiveGasPrice"]), - from_=Address.rpc_decode(val["from"]), - gas_used=rpc_decode_quantity(val["gasUsed"]), - to=Address.rpc_decode(val["to"]) if val["to"] else None, - transaction_hash=TxHash.rpc_decode(val["transactionHash"]), - transaction_index=rpc_decode_quantity(val["transactionIndex"]), - type_=rpc_decode_quantity(val["type"]), - succeeded=(rpc_decode_quantity(val["status"]) == 1), - logs=tuple(LogEntry.rpc_decode(ResponseDict(entry)) for entry in logs), - ) - - -def rpc_encode_quantity(val: int) -> str: - return hex(val) - - -def rpc_encode_data(val: bytes) -> str: - return "0x" + val.hex() - - -def rpc_encode_block(val: int | Block) -> str: - if isinstance(val, Block): - return val.value - return rpc_encode_quantity(val) - - -def rpc_decode_quantity(val: Any) -> int: - if not isinstance(val, str): - raise RPCDecodingError("Encoded quantity must be a string") - if not val.startswith("0x"): - raise RPCDecodingError("Encoded quantity must start with `0x`") - try: - return int(val, 16) - except ValueError as exc: - raise RPCDecodingError(f"Could not convert encoded quantity to an integer: {exc}") from exc - - -def rpc_decode_data(val: Any) -> bytes: - if not isinstance(val, str): - raise RPCDecodingError("Encoded data must be a string") - if not val.startswith("0x"): - raise RPCDecodingError("Encoded data must start with `0x`") - try: - return bytes.fromhex(val[2:]) - except ValueError as exc: - raise RPCDecodingError(f"Could not convert encoded data to bytes: {exc}") from exc - - -def rpc_decode_block(val: Any) -> int | str: - try: - Block(val) # check if it's one of the enum's values - # `Block` only has string values - return cast(str, val) - except ValueError: - pass - - return rpc_decode_quantity(val) - - -def rpc_decode_bool(val: Any) -> bool: - if not isinstance(val, bool): - raise RPCDecodingError("Encoded boolean must be `true` or `false`") - return val + def invalid_request(cls) -> "RPCError": + return cls(ErrorCode(RPCErrorCode.INVALID_REQUEST.value), "invalid json request") + + +# EIP-2930 transaction +@dataclass +class Type2Transaction: + # "type": 2 + chain_id: int + value: Amount + gas: int + max_fee_per_gas: Amount + max_priority_fee_per_gas: Amount + nonce: int + to: None | Address = None + data: None | bytes = None + + +@dataclass +class EthCallParams: + """Transaction fields for ``eth_call``.""" + + to: Address + from_: None | Address = None + gas: None | int = None + gas_price: int = 0 + value: Amount = Amount(0) + data: None | bytes = None + + +@dataclass +class EstimateGasParams: + """Transaction fields for ``eth_estimateGas``.""" + + from_: Address + to: None | Address = None + gas: None | int = None + gas_price: int = 0 + nonce: None | int = None + value: Amount = Amount(0) + data: None | bytes = None + + +@dataclass +class FilterParams: + """Filter parameters for ``eth_getLogs`` or ``eth_newFilter``.""" + + from_block: None | int | Block = None + to_block: None | int | Block = None + address: None | Address | tuple[Address, ...] = None + topics: None | tuple[None | LogTopic | tuple[LogTopic, ...], ...] = None diff --git a/pons/_fallback_provider.py b/pons/_fallback_provider.py index 9c3c5f7..8b2ec53 100644 --- a/pons/_fallback_provider.py +++ b/pons/_fallback_provider.py @@ -2,7 +2,8 @@ from collections.abc import AsyncIterator, Iterable from contextlib import AsyncExitStack, asynccontextmanager -from ._provider import JSON, InvalidResponse, Provider, ProviderSession, RPCError +from ._entities import RPCError +from ._provider import JSON, InvalidResponse, Provider, ProviderSession class FallbackStrategy(ABC): diff --git a/pons/_http_provider_server.py b/pons/_http_provider_server.py index 2a13030..26ad2b5 100644 --- a/pons/_http_provider_server.py +++ b/pons/_http_provider_server.py @@ -11,7 +11,9 @@ from starlette.routing import Route from trio_typing import TaskStatus -from ._provider import JSON, HTTPProvider, Provider, RPCError +from ._entities import RPCError +from ._provider import JSON, HTTPProvider, Provider +from ._serialization import unstructure def parse_request(request: JSON) -> tuple[JSON, str, list[JSON]]: @@ -46,7 +48,7 @@ async def process_request(provider: Provider, request: JSON) -> tuple[HTTPStatus try: request_id, result = await process_request_inner(provider, request) except RPCError as exc: - return HTTPStatus.BAD_REQUEST, {"jsonrpc": "2.0", "error": exc.to_json()} + return HTTPStatus.BAD_REQUEST, {"jsonrpc": "2.0", "error": unstructure(exc)} return HTTPStatus.OK, {"jsonrpc": "2.0", "id": request_id, "result": result} diff --git a/pons/_local_provider.py b/pons/_local_provider.py index 6578a78..a6b0d28 100644 --- a/pons/_local_provider.py +++ b/pons/_local_provider.py @@ -10,8 +10,8 @@ from alysis import RPCError as AlysisRPCError from eth_account import Account -from ._entities import Amount -from ._provider import JSON, Provider, ProviderSession, RPCError +from ._entities import Amount, ErrorCode, RPCError +from ._provider import JSON, Provider, ProviderSession from ._signer import AccountSigner, Signer @@ -62,7 +62,7 @@ def rpc(self, method: str, *args: Any) -> JSON: try: return self._rpc_node.rpc(method, *args) except AlysisRPCError as exc: - raise RPCError(exc.code, exc.message, exc.data) from exc + raise RPCError(ErrorCode(exc.code), exc.message, exc.data) from exc @asynccontextmanager async def session(self) -> AsyncIterator["LocalProviderSession"]: diff --git a/pons/_provider.py b/pons/_provider.py index 6a08543..5c612dc 100644 --- a/pons/_provider.py +++ b/pons/_provider.py @@ -1,45 +1,14 @@ from abc import ABC, abstractmethod -from collections.abc import AsyncIterator, Iterable, Mapping +from collections.abc import AsyncIterator, Mapping from contextlib import asynccontextmanager -from enum import Enum from http import HTTPStatus from json import JSONDecodeError from typing import cast import httpx -# TODO: the doc entry had to be written manually for this type because of Sphinx limitations. -JSON = None | bool | int | float | str | Iterable["JSON"] | Mapping[str, "JSON"] - - -class RPCErrorCode(Enum): - """Known RPC error codes returned by providers.""" - - # This is our placeholder value, shouldn't be encountered in a remote server response - UNKNOWN_REASON = 0 - """An error code whose description is not present in this enum.""" - - SERVER_ERROR = -32000 - """Reserved for implementation-defined server-errors. See the message for details.""" - - INVALID_REQUEST = -32600 - """The JSON sent is not a valid Request object.""" - - METHOD_NOT_FOUND = -32601 - """The method does not exist / is not available.""" - - INVALID_PARAMETER = -32602 - """Invalid method parameter(s).""" - - EXECUTION_ERROR = 3 - """Contract transaction failed during execution. See the data for details.""" - - @classmethod - def from_int(cls, val: int) -> "RPCErrorCode": - try: - return cls(val) - except ValueError: - return cls.UNKNOWN_REASON +from ._entities import RPCError +from ._serialization import JSON, StructuringError, structure class InvalidResponse(Exception): @@ -70,66 +39,6 @@ def __str__(self) -> str: return f"HTTP status {self.status}: {self.message}" -class RPCError(Exception): - """A wrapper for a call execution error returned as a proper RPC response.""" - - @classmethod - def from_json(cls, response: JSON) -> "RPCError": - error = ResponseDict(response) - if "data" in error: - data = error["data"] - if data is not None and not isinstance(data, str): - raise InvalidResponse( - f"Error data must be a string or None, got {type(data)} ({data})" - ) - else: - data = None - - error_code = error["code"] - if isinstance(error_code, str): - code = int(error_code) - elif isinstance(error_code, int): - code = error_code - else: - raise InvalidResponse( - "Error code must be an integer (possibly string-encoded), " - f"got {type(error_code)} ({error_code})" - ) - - message = error["message"] - if not isinstance(message, str): - raise InvalidResponse( - f"Error message must be a string, got {type(message)} ({message})" - ) - - return cls(code, message, data) - - @classmethod - def invalid_request(cls) -> "RPCError": - return cls(RPCErrorCode.INVALID_REQUEST.value, "invalid json request") - - @classmethod - def method_not_found(cls, method: str) -> "RPCError": - return cls( - RPCErrorCode.METHOD_NOT_FOUND.value, - f"The method {method} does not exist/is not available", - ) - - def __init__(self, code: int, message: str, data: None | str = None): - # Taking an integer and not `RPCErrorCode` here - # since the codes may differ between providers. - super().__init__(code, message, data) - self.code = code - self.message = message - self.data = data - - def to_json(self) -> JSON: - result = {"code": self.code, "message": self.message} - if self.data: - result["data"] = self.data - return result - - class Provider(ABC): """The base class for JSON RPC providers.""" @@ -145,28 +54,6 @@ async def session(self) -> AsyncIterator["ProviderSession"]: yield # type: ignore[misc] -class ResponseDict: - """ - A wrapper for dictionaries allowing as to narrow down KeyErrors - resulting from a JSON object of an incorrect format. - """ - - def __init__(self, obj: JSON): - if not isinstance(obj, dict): - raise InvalidResponse(f"Expected a dictionary as a response, got {type(obj).__name__}") - self._obj = cast(dict[str, JSON], obj) - - def __contains__(self, field: str) -> bool: - return field in self._obj - - def __getitem__(self, field: str) -> JSON: - try: - contents = self._obj[field] - except KeyError as exc: - raise InvalidResponse(f"Expected field `{field}` is missing from the result") from exc - return contents - - class ProviderSession(ABC): """ The base class for provider sessions. @@ -203,13 +90,6 @@ async def rpc_at_pin(self, path: tuple[int, ...], method: str, *args: JSON) -> J raise ValueError(f"Unexpected provider path: {path}") return await self.rpc(method, *args) - async def rpc_dict(self, method: str, *args: JSON) -> None | ResponseDict: - """Calls the given RPC method expecting to get a dictionary (or ``null``) in response.""" - result = await self.rpc(method, *args) - if result is None: - return None - return ResponseDict(result) - class HTTPProvider(Provider): """A provider for RPC via HTTP(S).""" @@ -255,7 +135,14 @@ async def rpc(self, method: str, *args: JSON) -> JSON: # Note that the Eth-side errors (e.g. transaction having been reverted) # will have the HTTP status 200, so we are checking for the "error" field first. if "error" in response_json: - raise RPCError.from_json(response_json["error"]) + try: + error = structure(RPCError, response_json["error"]) + except StructuringError as exc: + raise InvalidResponse( + f"Failed to parse an error response: {response_json}" + ) from exc + + raise error if status == HTTPStatus.OK: if "result" in response_json: diff --git a/pons/_serialization.py b/pons/_serialization.py new file mode 100644 index 0000000..156c715 --- /dev/null +++ b/pons/_serialization.py @@ -0,0 +1,177 @@ +"""Ethereum RPC schema.""" + +from collections.abc import Generator, Mapping, Sequence +from types import MappingProxyType, NoneType, UnionType +from typing import Any, TypeVar, Union, cast + +from compages import ( + StructureDictIntoDataclass, + Structurer, + StructuringError, + UnstructureDataclassToDict, + Unstructurer, + simple_structure, + simple_typechecked_unstructure, + structure_into_bool, + structure_into_int, + structure_into_list, + structure_into_none, + structure_into_str, + structure_into_tuple, + structure_into_union, + unstructure_as_bool, + unstructure_as_int, + unstructure_as_list, + unstructure_as_none, + unstructure_as_str, + unstructure_as_tuple, + unstructure_as_union, +) + +from ._entities import ( + Address, + Block, + ErrorCode, + Type2Transaction, + TypedData, + TypedQuantity, +) + +# TODO: the doc entry had to be written manually for this type because of Sphinx limitations. +JSON = None | bool | int | float | str | Sequence["JSON"] | Mapping[str, "JSON"] +"""Values serializable to JSON.""" + + +def _structure_into_bytes(_structurer: Structurer, _structure_into: Any, val: Any) -> bytes: + if not isinstance(val, str) or not val.startswith("0x"): + raise StructuringError("The value must be a 0x-prefixed hex-encoded data") + try: + return bytes.fromhex(val[2:]) + except ValueError as exc: + raise StructuringError(str(exc)) from exc + + +def _structure_into_typed_data( + _structurer: Structurer, structure_into: type[TypedData], val: Any +) -> TypedData: + data = _structure_into_bytes(_structurer, structure_into, val) + return structure_into(data) + + +def _structure_into_typed_quantity( + _structurer: Structurer, structure_into: type[TypedQuantity], val: Any +) -> TypedQuantity: + if not isinstance(val, str) or not val.startswith("0x"): + raise StructuringError("The value must be a 0x-prefixed hex-encoded integer") + int_val = int(val, 0) + return structure_into(int_val) + + +def _structure_into_int_common(val: Any) -> int: + if not isinstance(val, str) or not val.startswith("0x"): + raise StructuringError("The value must be a 0x-prefixed hex-encoded integer") + return int(val, 0) + + +@simple_structure +def _structure_into_int(val: Any) -> int: + return _structure_into_int_common(val) + + +def _unstructure_type2tx( + unstructurer: Unstructurer, _unstructure_as: type[Type2Transaction], obj: Type2Transaction +) -> Generator[Type2Transaction, dict[str, JSON], JSON]: + json = yield obj + json["type"] = unstructurer.unstructure_as(int, 2) + return json + + +@simple_typechecked_unstructure +def _unstructure_typed_quantity(obj: TypedQuantity) -> str: + return hex(int(obj)) + + +@simple_typechecked_unstructure +def _unstructure_typed_data(obj: TypedData) -> str: + return "0x" + bytes(obj).hex() + + +@simple_typechecked_unstructure +def _unstructure_address(obj: Address) -> str: + return obj.checksum + + +@simple_typechecked_unstructure +def _unstructure_block(obj: Block) -> str: + return obj.value + + +@simple_typechecked_unstructure +def _unstructure_int_to_hex(obj: int) -> str: + return hex(obj) + + +@simple_typechecked_unstructure +def _unstructure_bytes_to_hex(obj: bytes) -> str: + return "0x" + obj.hex() + + +def _to_camel_case(name: str, _metadata: MappingProxyType[Any, Any]) -> str: + if name.endswith("_"): + name = name[:-1] + parts = name.split("_") + return parts[0] + "".join(part.capitalize() for part in parts[1:]) + + +STRUCTURER = Structurer( + { + TypedData: _structure_into_typed_data, + TypedQuantity: _structure_into_typed_quantity, + ErrorCode: structure_into_int, + int: _structure_into_int, + str: structure_into_str, + bool: structure_into_bool, + bytes: _structure_into_bytes, + list: structure_into_list, + tuple: structure_into_tuple, + UnionType: structure_into_union, + Union: structure_into_union, + NoneType: structure_into_none, + }, + [StructureDictIntoDataclass(_to_camel_case)], +) + +UNSTRUCTURER = Unstructurer( + { + TypedData: _unstructure_typed_data, + TypedQuantity: _unstructure_typed_quantity, + Address: _unstructure_address, + Block: _unstructure_block, + ErrorCode: unstructure_as_int, + Type2Transaction: _unstructure_type2tx, + int: _unstructure_int_to_hex, + bytes: _unstructure_bytes_to_hex, + bool: unstructure_as_bool, + str: unstructure_as_str, + NoneType: unstructure_as_none, + list: unstructure_as_list, + UnionType: unstructure_as_union, + Union: unstructure_as_union, + tuple: unstructure_as_tuple, + }, + [UnstructureDataclassToDict(_to_camel_case)], +) + + +_T = TypeVar("_T") + + +def structure(structure_into: type[_T], obj: JSON) -> _T: + """Structures incoming JSON data.""" + return STRUCTURER.structure_into(structure_into, obj) + + +def unstructure(obj: Any, unstructure_as: Any = None) -> JSON: + """Unstructures data into JSON-serializable values.""" + # The result is `JSON` by virtue of the hooks we defined + return cast(JSON, UNSTRUCTURER.unstructure_as(unstructure_as or type(obj), obj)) diff --git a/pyproject.toml b/pyproject.toml index 955998e..c87e436 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -137,6 +137,8 @@ ignore = [ "PLR0913", # Conflicts with the formatter, according to the warning. "ISC001", + # Too many false positives, since Ruff cannot tell that the object is immutable. + "RUF009", ] # Allow autofix for all enabled rules (when `--fix`) is provided. diff --git a/tests/test_client.py b/tests/test_client.py index 19d3643..5b35c5c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -29,8 +29,7 @@ from pons._abi_types import encode_args, keccak from pons._client import BadResponseFormat, ProviderError, TransactionFailed from pons._contract_abi import PANIC_ERROR -from pons._entities import rpc_encode_data -from pons._provider import RPCError +from pons._entities import RPCError, TxInfo @pytest.fixture @@ -73,7 +72,7 @@ def mock_rpc(*_args): async def test_net_version_type_check(local_provider, session): # Provider returning a bad value with monkeypatched(local_provider, "rpc", lambda *_args: 0): - with pytest.raises(BadResponseFormat, match="net_version: expected a string result"): + with pytest.raises(BadResponseFormat, match="net_version: The value must be a string"): await session.net_version() @@ -495,14 +494,14 @@ async def test_get_block(session, root_signer, another_signer): await session.transfer(root_signer, another_signer.address, to_transfer) block_info = await session.eth_get_block_by_number(1, with_transactions=True) - assert block_info.transactions is not None + assert all(isinstance(tx, TxInfo) for tx in block_info.transactions) block_info2 = await session.eth_get_block_by_hash(block_info.hash_, with_transactions=True) assert block_info2 == block_info # no transactions block_info = await session.eth_get_block_by_number(1) - assert block_info.transactions is None + assert all(isinstance(tx, TxHash) for tx in block_info.transactions) # non-existent block block_info = await session.eth_get_block_by_number(100, with_transactions=True) @@ -532,8 +531,8 @@ async def test_get_block_pending(local_provider, session, root_signer, another_s assert block_info.transactions[0].value == Amount.ether(10) block_info = await session.eth_get_block_by_number(Block.PENDING, with_transactions=False) - assert len(block_info.transaction_hashes) == 1 - assert block_info.transaction_hashes[0] == tx_hash + assert len(block_info.transactions) == 1 + assert block_info.transactions[0] == tx_hash async def test_eth_get_transaction_by_hash(local_provider, session, root_signer, another_signer): @@ -609,7 +608,8 @@ def mock_rpc(_method, *_args): monkeypatch.setattr(local_provider, "rpc", mock_rpc) with pytest.raises( - BadResponseFormat, match=r"eth_getFilterChanges: Expected a list as a response, got dict" + BadResponseFormat, + match=r"eth_getFilterChanges: Can only structure a tuple or a list into a tuple generic", ): await session.eth_get_filter_changes(block_filter) @@ -669,6 +669,21 @@ async def test_eth_get_logs( == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics ) + entries = await session.eth_get_logs( + source=[contract1.address, contract2.address], from_block=0 + ) + assert len(entries) == 2 + assert entries[0].address == contract1.address + assert entries[1].address == contract2.address + assert ( + normalize_topics(entries[0].topics) + == contract1.abi.event.Deposit(root_signer.address, b"1234").topics + ) + assert ( + normalize_topics(entries[1].topics) + == contract2.abi.event.Deposit2(another_signer.address, b"4567").topics + ) + # Test an invalid response def mock_rpc(_method, *_args): @@ -677,7 +692,8 @@ def mock_rpc(_method, *_args): monkeypatch.setattr(local_provider, "rpc", mock_rpc) with pytest.raises( - BadResponseFormat, match=r"eth_getLogs: Expected a list as a response, got dict" + BadResponseFormat, + match=r"eth_getLogs: Can only structure a tuple or a list into a tuple generic", ): await session.eth_get_logs(source=contract2.address) @@ -1099,9 +1115,7 @@ def mock_rpc(method, *args): if method == "eth_estimateGas": # Invalid selector data = PANIC_ERROR.selector + encode_args((abi.uint(256), 888)) - raise RPCError( - RPCErrorCode.EXECUTION_ERROR, "execution reverted", rpc_encode_data(data) - ) + raise RPCError(RPCErrorCode.EXECUTION_ERROR, "execution reverted", data) return orig_rpc(method, *args) with monkeypatched(local_provider, "rpc", mock_rpc): @@ -1114,9 +1128,7 @@ def mock_rpc(method, *args): if method == "eth_estimateGas": # Invalid selector data = b"1234" + encode_args((abi.uint(256), 1)) - raise RPCError( - RPCErrorCode.EXECUTION_ERROR, "execution reverted", rpc_encode_data(data) - ) + raise RPCError(RPCErrorCode.EXECUTION_ERROR, "execution reverted", data) return orig_rpc(method, *args) with monkeypatched(local_provider, "rpc", mock_rpc): @@ -1131,7 +1143,7 @@ def mock_rpc(method, *args): if method == "eth_estimateGas": # Invalid selector data = PANIC_ERROR.selector + encode_args((abi.uint(256), 0)) - raise RPCError(12345, "execution reverted", rpc_encode_data(data)) + raise RPCError(12345, "execution reverted", data) return orig_rpc(method, *args) with monkeypatched(local_provider, "rpc", mock_rpc): diff --git a/tests/test_entities.py b/tests/test_entities.py index 5680334..c447723 100644 --- a/tests/test_entities.py +++ b/tests/test_entities.py @@ -2,21 +2,8 @@ import pytest -from pons import Address, Amount, Block, BlockHash, TxHash -from pons._entities import ( - BlockInfo, - LogEntry, - LogTopic, - RPCDecodingError, - TxReceipt, - rpc_decode_block, - rpc_decode_bool, - rpc_decode_data, - rpc_decode_quantity, - rpc_encode_block, - rpc_encode_data, - rpc_encode_quantity, -) +from pons import Address, Amount, BlockHash, TxHash +from pons._entities import LogTopic def test_amount(): @@ -38,10 +25,6 @@ def test_amount(): with pytest.raises(ValueError, match="Amount must be non-negative, got -100"): Amount.wei(-100) - # Encoding - assert Amount.wei(100).rpc_encode() == "0x64" - assert Amount.rpc_decode("0x64") == Amount.wei(100) - assert Amount.wei(100) + Amount.wei(50) == Amount.wei(150) assert Amount.wei(100) - Amount.wei(50) == Amount.wei(50) assert Amount.wei(100) * 2 == Amount.wei(200) @@ -91,8 +74,6 @@ def test_address(): assert Address(random_addr).checksum == random_addr_checksum - assert Address(random_addr).rpc_encode() == random_addr_checksum - assert Address.rpc_decode(random_addr_checksum) == Address(random_addr) assert Address(random_addr) == Address(random_addr) assert Address(random_addr) != Address(os.urandom(20)) @@ -125,16 +106,12 @@ class MyAddress(Address): with pytest.raises(ValueError): # noqa: PT011 Address.from_hex(random_addr_checksum[:-1]) - with pytest.raises(RPCDecodingError, match="Address must be 20 bytes long, got 19"): - Address.rpc_decode("0x" + random_addr[:-1].hex()) - def test_typed_data(): # This is not covered by Address tests, since it overrides those methods data = os.urandom(32) tx_hash = TxHash(data) assert repr(tx_hash) == f'TxHash(bytes.fromhex("{data.hex()}"))' - assert tx_hash.rpc_encode() == "0x" + data.hex() def test_typed_data_lengths(): @@ -144,212 +121,3 @@ def test_typed_data_lengths(): TxHash(os.urandom(32)) BlockHash(os.urandom(32)) LogTopic(os.urandom(32)) - - -def test_decode_tx_receipt(): - address = Address(os.urandom(20)) - - tx_receipt_json = { - "transactionHash": "0xf105e4ee72d1538a4e10ee9584581e2b6f13cd9be82acd14e8edd65d954483c5", - "blockHash": "0x3f62ac76f9551dbf878c334657ce19a86881734cbf53f8ecd9c3afb9a22d5bee", - "blockNumber": "0xa38696", - "contractAddress": address.rpc_encode(), - "cumulativeGasUsed": "0x43641c", - "effectiveGasPrice": "0x45463c1c", - "from": "0x8d75f6db12c444e290db995f2650a68159364e25", - "gasUsed": "0x55f0", - "status": "0x1", - "to": None, - "transactionIndex": "0x18", - "type": "0x0", - "logs": [ - { - "address": "0x388c818ca8b9251b393131c08a736a67ccb19297", - "blockHash": "0x0a79eca9f5ca58a1d5d5030a0fabfdd8e815b8b77a9f223f74d59aa39596e1c7", - "blockNumber": "0x11e5883", - "data": "0x00000000000000000000000000000000000000000000000011b6b79503fb875d", - "logIndex": "0x187", - "removed": False, - "topics": ["0x27f12abfe35860a9a927b465bb3d4a9c23c8428174b83f278fe45ed7b4da2662"], - "transactionHash": ( - "0x7114b4da1a6ed391d5d781447ed443733dcf2b508c515b81c17379dea8a3c9af" - ), - "transactionIndex": "0x76", - } - ], - } - - tx_receipt = TxReceipt.rpc_decode(tx_receipt_json) - - assert tx_receipt.succeeded - assert tx_receipt.contract_address == address - - tx_receipt_json["contractAddress"] = None - tx_receipt_json["to"] = address.rpc_encode() - tx_receipt_json["status"] = "0x0" - - tx_receipt = TxReceipt.rpc_decode(tx_receipt_json) - - assert not tx_receipt.succeeded - assert tx_receipt.contract_address is None - assert tx_receipt.to == address - - tx_receipt_json["logs"] = 1 - with pytest.raises(RPCDecodingError, match="`logs` in a tx receipt must be an iterable, got 1"): - TxReceipt.rpc_decode(tx_receipt_json) - - -def test_encode_decode_quantity(): - val = 100 - assert rpc_encode_quantity(val) == hex(val) - assert rpc_decode_quantity(hex(val)) == val - - with pytest.raises(RPCDecodingError, match="Encoded quantity must be a string"): - rpc_decode_quantity(100) - - with pytest.raises(RPCDecodingError, match="Encoded quantity must start with `0x`"): - rpc_decode_quantity("616263") - - with pytest.raises(RPCDecodingError, match="Could not convert encoded quantity to an integer"): - rpc_decode_quantity("0xefgh") - - -def test_encode_decode_data(): - assert rpc_encode_data(b"abc") == "0x616263" - assert rpc_decode_data("0x616263") == b"abc" - - with pytest.raises(RPCDecodingError, match="Encoded data must be a string"): - rpc_decode_data(616263) - - with pytest.raises(RPCDecodingError, match="Encoded data must start with `0x`"): - rpc_decode_data("616263") - - with pytest.raises(RPCDecodingError, match="Could not convert encoded data to bytes"): - rpc_decode_data("0xefgh") - - -def test_encode_decode_block(): - val = 123 - assert rpc_encode_block(Block.LATEST) == "latest" - assert rpc_encode_block(Block.EARLIEST) == "earliest" - assert rpc_encode_block(Block.PENDING) == "pending" - assert rpc_encode_block(Block.SAFE) == "safe" - assert rpc_encode_block(Block.FINALIZED) == "finalized" - assert rpc_encode_block(val) == hex(val) - assert rpc_decode_block("latest") == "latest" - assert rpc_decode_block("earliest") == "earliest" - assert rpc_decode_block("pending") == "pending" - assert rpc_decode_block(hex(val)) == val - - -def test_decode_bool(): - assert rpc_decode_bool(True) is True - - with pytest.raises(RPCDecodingError, match="Encoded boolean must be `true` or `false`"): - rpc_decode_bool(1) - - -def test_decode_block_info(): - json_result = { - "baseFeePerGas": "0xbcdaf1db6", - "difficulty": "0x2c72989f9145c8", - "gasLimit": "0x1cb2a73", - "gasUsed": "0x50c7cc", - "hash": "0x477a8386bbcc43f54b0231317d6a95f62ab10909d2d985ac5957633090ae69a8", - "miner": "0x7f101fe45e6649a6fb8f3f8b43ed03d353f2b90c", - "nonce": "0x2c4139002b04ac83", - "number": "0xda5f7a", - "parentHash": "0xa6fc86d8fc22aa8c164fa713b010b71a9071a2b2bc75f39cd6ec1256a4291e33", - "size": "0x65ad", - "timestamp": "0x6220267c", - "totalDifficulty": "0x911c203aa627addcf39", - "transactions": [ - { - "blockHash": "0x477a8386bbcc43f54b0231317d6a95f62ab10909d2d985ac5957633090ae69a8", - "blockNumber": "0xda5f7a", - "from": "0x4ac69ded1859e5ead2bf2ed8875d9c65012ce198", - "gas": "0x5208", - "gasPrice": "0x13b9b49c00", - "hash": "0x62581a4b947c113ecfb463df4d268c9f5791d95c91993e052a110731e8542140", - "input": "0x", - "nonce": "0x7", - "to": "0xe925433cf352cdc9c80df0a84641f3906758f4dc", - "transactionIndex": "0x0", - "type": "0x0", - "value": "0x6851a3a375d1ec", - }, - { - "blockHash": "0x477a8386bbcc43f54b0231317d6a95f62ab10909d2d985ac5957633090ae69a8", - "blockNumber": "0xda5f7a", - "from": "0xf1fb5dea21337feb46963c29d04a95f6ca8b71e6", - "gas": "0xd0b3", - "gasPrice": "0xcf7b50fb6", - "hash": "0x4eeae930617ad553af25a809f11051451e7f4a2597af6e8eae6ed446b94d6532", - "input": "0x632a5607", - "maxFeePerGas": "0xcfad7001d", - "maxPriorityFeePerGas": "0x12a05f200", - "nonce": "0x1317", - "to": "0x2e9d63788249371f1dfc918a52f8d799f4a38c94", - "transactionIndex": "0x3", - "type": "0x2", - "value": "0x0", - }, - ], - } - - # Parse output with the transaction info - block_info = BlockInfo.rpc_decode(json_result) - assert block_info.transactions[0].block_hash == BlockHash.rpc_decode( - json_result["transactions"][0]["blockHash"] - ) - assert block_info.transaction_hashes[0] == TxHash.rpc_decode( - json_result["transactions"][0]["hash"] - ) - - # Parse output without the transaction info - json_result["transactions"] = [ - json_result["transactions"][0]["hash"], - json_result["transactions"][1]["hash"], - ] - block_info = BlockInfo.rpc_decode(json_result) - assert block_info.transactions is None - assert block_info.transaction_hashes[0] == TxHash.rpc_decode(json_result["transactions"][0]) - - # Parse output without any transactions - json_result["transactions"] = [] - block_info = BlockInfo.rpc_decode(json_result) - assert block_info.transactions == () - assert block_info.transaction_hashes == () - - json_result["transactions"] = 1 - with pytest.raises( - RPCDecodingError, match="`transactions` in a block info must be an iterable, got 1" - ): - BlockInfo.rpc_decode(json_result) - - -def test_decode_log_entry(): - log_entry_json = { - "address": "0x6ef0f6ca7a8f01e02593df24f7097889a249c31e", - "topics": [ - "0x8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0", - "0x0000000000000000000000000000000000000000000000000000000000000000", - "0x000000000000000000000000e2b8651bf50913057ff47fc4f02a8e12146083b8", - ], - "data": "0x", - "blockNumber": "0xa386df", - "transactionHash": "0x7c0301cec59c65dce24fe7bd007dd4ab1dbe6c7c0f029deb76f4fb2d4c004379", - "transactionIndex": "0x0", - "blockHash": "0x14fb839d874ee80c020e7f087f8e57d4c6a94d6af1e6c7c9544d1ef219b0873b", - "logIndex": "0x0", - "removed": False, - } - - log_entry = LogEntry.rpc_decode(log_entry_json) - assert log_entry.data == b"" - - log_entry_json["topics"] = 1 - with pytest.raises( - RPCDecodingError, match="`topics` in a log entry must be an iterable, got 1" - ): - LogEntry.rpc_decode(log_entry_json) diff --git a/tests/test_fallback_provider.py b/tests/test_fallback_provider.py index ef9f735..2105f48 100644 --- a/tests/test_fallback_provider.py +++ b/tests/test_fallback_provider.py @@ -6,8 +6,9 @@ import pytest from pons import CycleFallback, FallbackProvider, PriorityFallback, Unreachable +from pons._entities import RPCError from pons._fallback_provider import PriorityFallbackStrategy -from pons._provider import JSON, InvalidResponse, Provider, ProviderSession, RPCError +from pons._provider import JSON, InvalidResponse, Provider, ProviderSession def random_request(): diff --git a/tests/test_local_provider.py b/tests/test_local_provider.py index ef27d66..bda08d6 100644 --- a/tests/test_local_provider.py +++ b/tests/test_local_provider.py @@ -1,30 +1,8 @@ -# TODO (#60): expand the tests so that this file covered 100% of the respective submodule, -# and don't use high-level API. - -from pathlib import Path +# TODO (#60): expand the tests so that this file covered 100% of the respective submodule. import pytest -from pons import ( - AccountSigner, - Address, - Amount, - Block, - Client, - LocalProvider, - TxHash, - abi, - compile_contract_file, -) -from pons._abi_types import decode_args, encode_args -from pons._entities import ( - BlockInfo, - TxInfo, - rpc_decode_data, - rpc_encode_block, - rpc_encode_data, - rpc_encode_quantity, -) +from pons import AccountSigner, Amount, Client, LocalProvider # Masking the global fixtures to make this test self-contained @@ -51,21 +29,6 @@ def another_signer(): return AccountSigner.create() -def make_transfer_tx(provider, dest, amount, nonce): - return { - "type": rpc_encode_quantity(2), # EIP-2930 transaction - "chainId": provider.rpc("eth_chainId"), - "to": dest.rpc_encode(), - "value": amount.rpc_encode(), - # This is the fixed price for the transfer in Ethereum. - # Ideally we should take it from estimate_gas(), but for tests we short-circuit it. - "gas": rpc_encode_quantity(21000), - "maxFeePerGas": provider.rpc("eth_gasPrice"), - "maxPriorityFeePerGas": Amount.gwei(1).rpc_encode(), - "nonce": rpc_encode_quantity(nonce), - } - - async def test_root_balance(): amount = Amount.ether(123) provider = LocalProvider(root_balance=amount) @@ -77,165 +40,45 @@ async def test_root_balance(): async def test_auto_mine(provider, session, root_signer, another_signer): amount = Amount.ether(1) dest = another_signer.address - latest = rpc_encode_block(Block.LATEST) # Auto-mininig is the default behavior tx_hash = await session.broadcast_transfer(root_signer, dest, amount) receipt = await session.eth_get_transaction_receipt(tx_hash) assert receipt.succeeded - assert provider.rpc("eth_getBalance", dest.rpc_encode(), latest) == amount.rpc_encode() + assert await session.eth_get_balance(dest) == amount # Disable auto-mining. Now broadcasting the transaction does not automatically finalize it. provider.disable_auto_mine_transactions() tx_hash = await session.broadcast_transfer(root_signer, dest, amount) receipt = await session.eth_get_transaction_receipt(tx_hash) assert receipt is None - assert provider.rpc("eth_getBalance", dest.rpc_encode(), latest) == amount.rpc_encode() + assert await session.eth_get_balance(dest) == amount # Enable auto-mining back. The pending transactions are added to the block. provider.enable_auto_mine_transactions() receipt = await session.eth_get_transaction_receipt(tx_hash) assert receipt.succeeded - assert provider.rpc("eth_getBalance", dest.rpc_encode(), latest) == Amount.ether(2).rpc_encode() + assert await session.eth_get_balance(dest) == Amount.ether(2) async def test_snapshots(provider, session, root_signer, another_signer): amount = Amount.ether(1) double_amount = Amount.ether(2) dest = another_signer.address - latest = rpc_encode_block(Block.LATEST) await session.transfer(root_signer, dest, amount) snapshot_id = provider.take_snapshot() await session.transfer(root_signer, dest, amount) - assert provider.rpc("eth_getBalance", dest.rpc_encode(), latest) == double_amount.rpc_encode() + assert await session.eth_get_balance(dest) == double_amount provider.revert_to_snapshot(snapshot_id) - assert provider.rpc("eth_getBalance", dest.rpc_encode(), latest) == amount.rpc_encode() + assert await session.eth_get_balance(dest) == amount -def test_net_version(provider): - assert provider.rpc("net_version") == "1" +async def test_net_version(session): + assert await session.net_version() == "1" def test_eth_chain_id(): provider = LocalProvider(root_balance=Amount.ether(100), chain_id=0xABC) - # Something set in the depths of PyEVM by default. - # May be worth making it customizable at some point. assert provider.rpc("eth_chainId") == "0xabc" - - -def test_eth_get_balance(): - amount = Amount.ether(123) - provider = LocalProvider(root_balance=amount) - balance = provider.rpc( - "eth_getBalance", provider.root.address.rpc_encode(), rpc_encode_block(Block.LATEST) - ) - assert balance == rpc_encode_quantity(amount.as_wei()) - - -async def test_eth_get_transaction_count(provider, session, root_signer, another_signer): - address = root_signer.address.rpc_encode() - assert provider.rpc("eth_getTransactionCount", address, rpc_encode_block(Block.LATEST)) == "0x0" - await session.transfer(root_signer, another_signer.address, Amount.ether(1)) - assert provider.rpc("eth_getTransactionCount", address, rpc_encode_block(Block.LATEST)) == "0x1" - - -async def test_eth_send_raw_transaction(provider, root_signer, another_signer): - amount = Amount.ether(1) - dest = another_signer.address - latest = rpc_encode_block(Block.LATEST) - - tx = make_transfer_tx(provider, dest, amount, 0) - signed_tx = root_signer.sign_transaction(tx) - provider.rpc("eth_sendRawTransaction", rpc_encode_data(signed_tx)) - - # Test that the transaction came through - assert provider.rpc("eth_getBalance", dest.rpc_encode(), latest) == amount.rpc_encode() - - -async def test_eth_call(provider, session, root_signer): - path = Path(__file__).resolve().parent / "TestLocalProvider.sol" - compiled = compile_contract_file(path) - compiled_contract = compiled["BasicContract"] - - deployed_contract = await session.deploy(root_signer, compiled_contract.constructor()) - - result = provider.rpc( - "eth_call", - { - "to": deployed_contract.address.rpc_encode(), - "data": rpc_encode_data( - compiled_contract.abi.method.getState.selector + encode_args([abi.uint(256), 456]) - ), - }, - rpc_encode_block(Block.LATEST), - ) - - assert decode_args([abi.uint(256)], rpc_decode_data(result)) == (123 + 456,) - - # Use an explicit `from` field to cover the branch where it is substituted if missing. - result = provider.rpc( - "eth_call", - { - "from": root_signer.address.rpc_encode(), - "to": deployed_contract.address.rpc_encode(), - "data": rpc_encode_data( - compiled_contract.abi.method.getState.selector + encode_args([abi.uint(256), 456]) - ), - }, - rpc_encode_block(Block.LATEST), - ) - - assert decode_args([abi.uint(256)], rpc_decode_data(result)) == (123 + 456,) - - -async def test_eth_block_number(provider, session, root_signer, another_signer): - assert provider.rpc("eth_blockNumber") == "0x0" - await session.transfer(root_signer, another_signer.address, Amount.ether(1)) - assert provider.rpc("eth_blockNumber") == "0x1" - - -async def test_eth_get_transaction_by_hash(provider, root_signer, another_signer): - amount = Amount.ether(1) - dest = another_signer.address - - tx = make_transfer_tx(provider, dest, amount, 0) - signed_tx = root_signer.sign_transaction(tx) - tx_hash = TxHash.rpc_decode(provider.rpc("eth_sendRawTransaction", rpc_encode_data(signed_tx))) - - recorded_tx = provider.rpc("eth_getTransactionByHash", tx_hash.rpc_encode()) - - preserved_fields = [ - "type", - "chainId", - "nonce", - "value", - "gas", - "maxFeePerGas", - "maxPriorityFeePerGas", - ] - for field in preserved_fields: - assert recorded_tx[field] == tx[field] - - assert tx["to"] == Address.from_hex(recorded_tx["to"]).checksum - assert recorded_tx["blockNumber"] == "0x1" - - # Test non-existent transaction - assert provider.rpc("eth_getTransactionByHash", "0x" + "1" * 64) is None - - -async def test_eth_get_block_by_hash(provider, root_signer, another_signer): - amount = Amount.ether(1) - dest = another_signer.address - - tx = make_transfer_tx(provider, dest, amount, 0) - signed_tx = root_signer.sign_transaction(tx) - tx_hash = TxHash.rpc_decode(provider.rpc("eth_sendRawTransaction", rpc_encode_data(signed_tx))) - recorded_tx = TxInfo.rpc_decode(provider.rpc("eth_getTransactionByHash", tx_hash.rpc_encode())) - - block = BlockInfo.rpc_decode( - provider.rpc("eth_getBlockByHash", recorded_tx.block_hash.rpc_encode(), True) - ) - assert block.number == 1 - assert block.miner == Address.from_hex("0x0000000000000000000000000000000000000000") diff --git a/tests/test_provider.py b/tests/test_provider.py index d7218c5..1cb4f3c 100644 --- a/tests/test_provider.py +++ b/tests/test_provider.py @@ -13,14 +13,8 @@ _http_provider_server, # For monkeypatching purposes ) from pons._client import BadResponseFormat, ProviderError -from pons._provider import ( - HTTPError, - InvalidResponse, - Provider, - ProviderSession, - RPCError, - RPCErrorCode, -) +from pons._entities import RPCErrorCode +from pons._provider import HTTPError, Provider, ProviderSession @pytest.fixture @@ -46,47 +40,6 @@ async def test_dict_request(session, root_signer, another_signer): await session.transfer(root_signer, another_signer.address, Amount.ether(10)) -def test_rpc_error(): - error_code = 2 - - error = RPCError.from_json({"code": error_code, "message": "error", "data": "additional data"}) - assert error.code == error_code - assert error.data == "additional data" - assert error.to_json() == {"code": error_code, "message": "error", "data": "additional data"} - - error = RPCError.from_json({"code": error_code, "message": "error"}) - assert error.data is None - assert error.to_json() == {"code": error_code, "message": "error"} - - error = RPCError.from_json({"code": str(error_code), "message": "error"}) - assert error.code == error_code - - error = RPCError.invalid_request() - assert error.code == RPCErrorCode.INVALID_REQUEST.value - - error = RPCError.method_not_found("abc") - assert error.code == RPCErrorCode.METHOD_NOT_FOUND.value - - with pytest.raises( - InvalidResponse, match=r"Error data must be a string or None, got \(1\)" - ): - RPCError.from_json({"data": 1, "code": 2, "message": "error"}) - - with pytest.raises( - InvalidResponse, - match=( - r"Error code must be an integer \(possibly string-encoded\), " - r"got \(1\.0\)" - ), - ): - RPCError.from_json({"code": 1.0, "message": "error"}) - - with pytest.raises( - InvalidResponse, match=r"Error message must be a string, got \(1\)" - ): - RPCError.from_json({"code": 2, "message": 1}) - - async def test_dict_request_introspection(session, root_signer, another_signer): # This test covers the __contains__ method of ResponseDict. # It is invoked when the error response is checked for the "data" field, @@ -110,7 +63,7 @@ async def test_unexpected_response_type( monkeypatch.setattr(local_provider, "rpc", lambda _method, *_args: "something") - with pytest.raises(BadResponseFormat, match="Expected a dictionary as a response, got str"): + with pytest.raises(BadResponseFormat, match="Cannot structure into"): await session.eth_get_transaction_receipt(tx_hash) @@ -129,9 +82,7 @@ def mock_rpc(method, *args): monkeypatch.setattr(local_provider, "rpc", mock_rpc) - with pytest.raises( - BadResponseFormat, match="Expected field `status` is missing from the result" - ): + with pytest.raises(BadResponseFormat, match="status: Missing field"): await session.eth_get_transaction_receipt(tx_hash) @@ -180,7 +131,7 @@ async def faulty_process_request(*args, **kwargs): async def test_no_error_field(session, monkeypatch): - # Tests the handling of a badly formed success response without the "error" field. + # Tests the handling of a badly formed error response without the "error" field. orig_process_request = _http_provider_server.process_request @@ -195,6 +146,24 @@ async def faulty_process_request(*args, **kwargs): await session.net_version() +async def test_malformed_error_field(session, monkeypatch): + # Tests the handling of a badly formed error response + # where the "error" field cannot be parsed as an RPCError. + + orig_process_request = _http_provider_server.process_request + + async def faulty_process_request(*args, **kwargs): + status, response = await orig_process_request(*args, **kwargs) + del response["result"] + response["error"] = {"something_weird": 1} + return (HTTPStatus.BAD_REQUEST, response) + + monkeypatch.setattr(_http_provider_server, "process_request", faulty_process_request) + + with pytest.raises(BadResponseFormat, match=r"Failed to parse an error response"): + await session.net_version() + + async def test_result_is_not_a_dict(session, monkeypatch): # Tests the handling of a badly formed provider response that is not a dictionary. # Unfortunately we can't achieve that by just patching the provider, have to patch the server diff --git a/tests/test_serialization.py b/tests/test_serialization.py new file mode 100644 index 0000000..40d1ac2 --- /dev/null +++ b/tests/test_serialization.py @@ -0,0 +1,38 @@ +import os + +import pytest + +from pons import Address, Amount +from pons._serialization import StructuringError, structure + + +def test_structure_into_typed_quantity(): + assert structure(Amount, "0x123") == Amount(0x123) + + with pytest.raises( + StructuringError, match="The value must be a 0x-prefixed hex-encoded integer" + ): + structure(Amount, "abc") + + +def test_structure_into_int(): + assert structure(int, "0x123") == 0x123 + + with pytest.raises( + StructuringError, match="The value must be a 0x-prefixed hex-encoded integer" + ): + structure(int, "abc") + + +def test_structure_into_typed_data(): + address = os.urandom(20) + assert structure(Address, "0x" + address.hex()) == Address(address) + + with pytest.raises(StructuringError, match="The value must be a 0x-prefixed hex-encoded data"): + structure(Address, "abc") + + # The error text is weird + with pytest.raises( + StructuringError, match=r"non-hexadecimal number found in fromhex\(\) arg at position 0" + ): + structure(Address, "0xzz") From 07efeedda6af0da2cfeaeb7d517b11d8eb4106e5 Mon Sep 17 00:00:00 2001 From: Bogdan Opanchuk Date: Fri, 15 Mar 2024 17:08:52 -0700 Subject: [PATCH 3/3] Simplify a complicated event to facilitate tests PyEvm does not support opcode 0x5e (MCOPY) yet --- tests/TestContractFunctionality.sol | 27 +++++++++++++++------------ tests/test_contract_functionality.py | 12 ++++-------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/tests/TestContractFunctionality.sol b/tests/TestContractFunctionality.sol index 7baf92e..4d37208 100644 --- a/tests/TestContractFunctionality.sol +++ b/tests/TestContractFunctionality.sol @@ -68,35 +68,38 @@ contract Test { struct ByteInner { bytes4 inner1; - bytes inner2; + bytes10 inner2; } struct Foo { bytes4 foo1; bytes2[2] foo2; - bytes foo3; - string foo4; + bytes6 foo3; ByteInner inner; } event Complicated( bytes4 indexed x, - bytes indexed y, + bytes8 indexed y, Foo indexed u, ByteInner[2] indexed v ) anonymous; function emitComplicated() public { - bytes memory bytestring33len1 = "012345678901234567890123456789012"; - bytes memory bytestring33len2 = "-12345678901234567890123456789012"; - ByteInner memory inner1 = ByteInner("0123", bytestring33len1); - ByteInner memory inner2 = ByteInner("-123", bytestring33len2); bytes2 x = "aa"; bytes2 y = "bb"; - bytes2[2] memory foo2 = [x, y]; - Foo memory foo = Foo("4567", foo2, bytestring33len1, "\u1234\u1212", inner1); - ByteInner[2] memory inner_arr = [inner1, inner2]; - emit Complicated("aaaa", bytestring33len2, foo, inner_arr); + emit Complicated( + "aaaa", + "55555555", + Foo( + "4567", [x, y], + "444444", + ByteInner("0123", "3333333333") + ), + [ + ByteInner("0123", "1111111111"), + ByteInner("-123", "2222222222") + ]); } error MyError(address sender); diff --git a/tests/test_contract_functionality.py b/tests/test_contract_functionality.py index a43e99c..92090b3 100644 --- a/tests/test_contract_functionality.py +++ b/tests/test_contract_functionality.py @@ -161,14 +161,10 @@ async def test_complicated_event(session, root_signer, compiled_contracts): basic_contract = compiled_contracts["Test"] - bytestring33len1 = b"012345678901234567890123456789012" - bytestring33len2 = b"-12345678901234567890123456789012" - inner1 = [b"0123", bytestring33len1] - inner2 = [b"-123", bytestring33len2] - foo = [b"4567", [b"aa", b"bb"], bytestring33len1, "\u1234\u1212", inner1] - event_filter = basic_contract.abi.event.Complicated( - b"aaaa", bytestring33len2, foo, [inner1, inner2] - ) + inner1 = [b"0123", b"1111111111"] + inner2 = [b"-123", b"2222222222"] + foo = [b"4567", [b"aa", b"bb"], b"444444", [b"0123", b"3333333333"]] + event_filter = basic_contract.abi.event.Complicated(b"aaaa", b"55555555", foo, [inner1, inner2]) contract = await session.deploy(root_signer, basic_contract.constructor(123, 456))