diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3599e6c4..6a2ba5db 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -71,6 +71,7 @@ jobs: "tests/integration/test_token_client.py", "tests/integration/test_websockets.py", "tests/integration/test_recent_performance_samples.py", + "tests/integration/test_memo.py", "tests/unit", "src", # doctests ] diff --git a/.gitignore b/.gitignore index c0b7b0b9..f124d962 100644 --- a/.gitignore +++ b/.gitignore @@ -31,8 +31,11 @@ dist # solana validator logs test-ledger/ -#IDE Files +# IDE Files .idea/ # pytest coverage .coverage + +# manual tests +testing.py diff --git a/CHANGELOG.md b/CHANGELOG.md index feda59e6..d5db5361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,25 @@ ## Fixed -- Use latest Solders version to make objects pickleable again [(#252)](https://github.com/michaelhly/solana-py/pull/252) +- Use latest Solders version to make objects pickleable again [(#252)](https://github.com/michaelhly/solana-py/pull/252). + + +## Changed + +- Updated httpx to fix critical vulnerability [(#248)](https://github.com/michaelhly/solana-py/pull/248). +- Updated pytest, websockets, pytest-docker, pytest-asyncio to latest. [(#254)](https://github.com/michaelhly/solana-py/pull/254). +- Updated apischema to latest. [(#254)](https://github.com/michaelhly/solana-py/pull/254). + + +## Added + +- Added `get_latest_blockhash` RPC Call. [(#254)](https://github.com/michaelhly/solana-py/pull/254). +- Added `get_fee_for_message` RPC Call. [(#254)](https://github.com/michaelhly/solana-py/pull/254). +- Added confirmation strategy which checks if the transaction has exceeded last valid blockheight. [(#254)](https://github.com/michaelhly/solana-py/pull/254). +- Added `asyncio_mode = auto` in pytest.ini. [(#248)](https://github.com/michaelhly/solana-py/pull/254). +- Added an optional `verify_signature` bool when `transaction.serialize()` is called [(#249)](https://github.com/michaelhly/solana-py/pull/249). +- Added Memo program [(#249)](https://github.com/michaelhly/solana-py/pull/249). + ## [0.24.0] - 2022-06-04 diff --git a/README.md b/README.md index 09fb73cf..337507ee 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ from asyncstdlib import enumerate from solana.rpc.websocket_api import connect async def main(): - async with connect("ws://api.devnet.solana.com") as websocket: + async with connect("wss://api.devnet.solana.com") as websocket: await websocket.logs_subscribe() first_resp = await websocket.recv() subscription_id = first_resp.result @@ -95,7 +95,7 @@ async def main(): await websocket.logs_unsubscribe(subscription_id) # Alternatively, use the client as an infinite asynchronous iterator: - async with connect("ws://api.devnet.solana.com") as websocket: + async with connect("wss://api.devnet.solana.com") as websocket: await websocket.logs_subscribe() first_resp = await websocket.recv() subscription_id = first_resp.result diff --git a/poetry.lock b/poetry.lock index d34e06ad..b054bcf9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,8 +18,8 @@ trio = ["trio (>=0.16)"] [[package]] name = "apischema" -version = "0.16.6" -description = "JSON (de)serialization, *GraphQL* and JSON schema generation using Python typing." +version = "0.17.5" +description = "JSON (de)serialization, GraphQL and JSON schema generation using Python typing." category = "main" optional = false python-versions = ">=3.6" @@ -162,7 +162,7 @@ python-versions = "~=3.5" [[package]] name = "certifi" -version = "2022.5.18.1" +version = "2022.6.15" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -204,7 +204,7 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.5" description = "Cross-platform colored terminal text." category = "dev" optional = false @@ -469,11 +469,11 @@ plugins = ["setuptools"] [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.2" description = "A very fast and expressive template engine." category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] MarkupSafe = ">=2.0" @@ -602,14 +602,14 @@ mkdocs = ">=1.1" [[package]] name = "mkdocs-material" -version = "8.2.7" +version = "8.2.6" description = "A Material Design theme for MkDocs" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -jinja2 = ">=2.11.1,<3.1" +jinja2 = ">=2.11.1" markdown = ">=3.2" mkdocs = ">=1.2.3" mkdocs-material-extensions = ">=1.0" @@ -883,11 +883,11 @@ python-versions = ">=3.7" [[package]] name = "pytest" -version = "6.2.5" +version = "7.1.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} @@ -898,24 +898,25 @@ iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" py = ">=1.8.2" -toml = "*" +tomli = ">=1.0.0" [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.16.0" -description = "Pytest support for asyncio." +version = "0.18.3" +description = "Pytest support for asyncio" category = "dev" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.7" [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=6.1.0" +typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage", "hypothesis (>=5.7.1)"] +testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" @@ -934,7 +935,7 @@ testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtuale [[package]] name = "pytest-docker" -version = "0.10.3" +version = "0.12.0" description = "Simple pytest fixtures for Docker and docker-compose based tests" category = "dev" optional = false @@ -943,7 +944,7 @@ python-versions = ">=3.6" [package.dependencies] attrs = ">=19,<22" docker-compose = ">=1.27.3,<2.0" -pytest = ">=4.0,<7.0" +pytest = ">=4.0,<8.0" [package.extras] tests = ["requests (>=2.22.0,<3.0)", "pytest-pylint (>=0.14.1,<1.0)", "pytest-pycodestyle (>=2.0.0,<3.0)"] @@ -1213,7 +1214,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "c49e8e969c428102ef7df977a295c16b2f8c3da6473518b6ee9a5b17f720171a" +content-hash = "2aa504f2568881a051831233109ac41733c2d5aa8a58bd9f81d74d9ffc50678a" [metadata.files] anyio = [ @@ -1221,8 +1222,42 @@ anyio = [ {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, ] apischema = [ - {file = "apischema-0.16.6-py3-none-any.whl", hash = "sha256:b1ffcc19831fceb99c175ce53d81125bb46b8b22a019f4d8307f6d23f13e5647"}, - {file = "apischema-0.16.6.tar.gz", hash = "sha256:5e53830269d17a3586103c71d7961f23df5fe8844f93afbcc3e7a1a7cbac0c75"}, + {file = "apischema-0.17.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21a1ba34ab4c2638f67589eb71baa76c126698569090194ea1133ed8fa044da5"}, + {file = "apischema-0.17.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cba5ca8627e39ec357e63e1ca3fd4b1c7ab0c2a7000d2f0d47cdf445b1b870a6"}, + {file = "apischema-0.17.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:37195236e8fc8750673584dd2273f997b8ebff534a35e5fa23d3cc1a6b19d42a"}, + {file = "apischema-0.17.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5cef4588d1167a343b788d39596befa58b1df9c234bc9193dee1b82e4e54a29e"}, + {file = "apischema-0.17.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:874febeb8f2ca1169dccc3f5f1d09ffc667ca913aa2015ccf9db87dcb2306e94"}, + {file = "apischema-0.17.5-cp310-cp310-win32.whl", hash = "sha256:a2c389e9f17c0ba49513ad2dc87bc5196cfa8e1b585caaf9b02099cfe1b80958"}, + {file = "apischema-0.17.5-cp310-cp310-win_amd64.whl", hash = "sha256:552466c3c9f7c9105599a9501adf16aca3e815dae296567a0d5db5da445a09c3"}, + {file = "apischema-0.17.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a9137c0b355439dab63c015744579f83a982133afc833ad600368297b2564f0e"}, + {file = "apischema-0.17.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d60833bd9c7820716bee6ac69d4df9f59f5f6f01e46ff2b13ff2a63670316c9d"}, + {file = "apischema-0.17.5-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10e0aea9766bb27b5e68c71e6f61c6564b199d70e19683cbea94429e8bce548c"}, + {file = "apischema-0.17.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:3f94d23815eef966fa802daaca994ba1382a1a2ee2ef28c0c84069b241d7f088"}, + {file = "apischema-0.17.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:03e7d4f8e64096159353c81564091f72ed9925d99f77fb022b715851b64d8905"}, + {file = "apischema-0.17.5-cp36-cp36m-win32.whl", hash = "sha256:ac12b0038ace744b692db067fd0a15d5f0b417f0452c66bfd8103c022d0fd818"}, + {file = "apischema-0.17.5-cp36-cp36m-win_amd64.whl", hash = "sha256:9fcddde7416c514a63adf02369d925030dcb74bee95bd77780dd9177faab136d"}, + {file = "apischema-0.17.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b03ad6ed25c9247945dcacd7f4adb63a23774534424920c87c52a8bb9f38485f"}, + {file = "apischema-0.17.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4669066ade969ed1b28cd5f30bf7f56dda494f20a2f1ad7294cd9aefc29d0c57"}, + {file = "apischema-0.17.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:caa72d8f939e978daef305ccb429213d89d40ddfae74424231bc71bdcfd44651"}, + {file = "apischema-0.17.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:0d4eabf40a31fd60144e3f21576efcf63fd14c2aed7ab45dcfdf2c1ba6692a6f"}, + {file = "apischema-0.17.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ef0a655aa82454ce5482a50cd8a4c14cd4f7a849ab3a10b4bc09ca018b85946"}, + {file = "apischema-0.17.5-cp37-cp37m-win32.whl", hash = "sha256:ff29a5b86067c8664dd3e6ae9dec2f687bab0498cb0f90ce543676ad0a79bced"}, + {file = "apischema-0.17.5-cp37-cp37m-win_amd64.whl", hash = "sha256:b1464e96e9bff4f3f0449ab7ad26dd28e560fcc355304abcd871c6cfbd46cea1"}, + {file = "apischema-0.17.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:72eab80f29bc55cd23c685e909ffa21c9db8b545e3a4b1d6961e65ead11fc1d0"}, + {file = "apischema-0.17.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b285e05f5a791d376f0af04964201a4e16218a860f09a8987b7e81638633bde6"}, + {file = "apischema-0.17.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3191c72113709647e3f56ae354468e50b5f93618cfc36bd0709ca1ec9f31a765"}, + {file = "apischema-0.17.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ca8be57dd31bd4ace958fd46b12ddb99e007b3b6ebbbfabedb8989e35dfca965"}, + {file = "apischema-0.17.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9d6fdaf63db3e823f7d09548cbcaf279d7b98c4f630b875efdad91a02abbe45b"}, + {file = "apischema-0.17.5-cp38-cp38-win32.whl", hash = "sha256:de1f31687ab373031d161666fd36f99a013d1e9fea91b5f2de7bfc01db9e15db"}, + {file = "apischema-0.17.5-cp38-cp38-win_amd64.whl", hash = "sha256:a4197aac0bffca619eeae3d162ae425647032e0f7a453751509d6fa54fc61f6a"}, + {file = "apischema-0.17.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0b8b85f90aa2af05171f9fb48dcfb2291c14e01858d840a34579ee9930e2ac03"}, + {file = "apischema-0.17.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9576d4c90a2771275e3d4fd1878cb41f354267646a892e3ccc9b86d2991acc7"}, + {file = "apischema-0.17.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:763304928b7275b1c9e6efced5287bd4721b818dcc277c35820dd27249d05462"}, + {file = "apischema-0.17.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b82976a3700c4065da1c2619caac06111251809a9aa74c8f19e894d98896ba16"}, + {file = "apischema-0.17.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:25a7d56ca644a0ac01e4e8e061bcafd9c5b040e0e8891900f2030068ca437003"}, + {file = "apischema-0.17.5-cp39-cp39-win32.whl", hash = "sha256:650602ad12669a9537b88f2a5850199937bb4da87b9b4ef160bc1eb4e74647e3"}, + {file = "apischema-0.17.5-cp39-cp39-win_amd64.whl", hash = "sha256:19910f39ebf3c2c7663772639400432831cbbfb9495b54415366ef5f24a62f97"}, + {file = "apischema-0.17.5.tar.gz", hash = "sha256:958175d3a2608ff49d4e3ba01bde4888837f4efd7418e27770d2c4b79648b41d"}, ] astroid = [ {file = "astroid-2.11.6-py3-none-any.whl", hash = "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9"}, @@ -1311,8 +1346,8 @@ cachetools = [ {file = "cachetools-4.2.4.tar.gz", hash = "sha256:89ea6f1b638d5a73a4f9226be57ac5e4f399d22770b92355f92dcb0f7f001693"}, ] certifi = [ - {file = "certifi-2022.5.18.1-py3-none-any.whl", hash = "sha256:f1d53542ee8cbedbe2118b5686372fb33c297fcd6379b050cca0ef13a597382a"}, - {file = "certifi-2022.5.18.1.tar.gz", hash = "sha256:9c5705e395cd70084351dd8ad5c41e65655e08ce46f2ec9cf6c2c08390f71eb7"}, + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] cffi = [ {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, @@ -1375,8 +1410,8 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] construct = [ {file = "construct-2.10.67.tar.gz", hash = "sha256:730235fedf4f2fee5cfadda1d14b83ef1bf23790fb1cc579073e10f70a050883"}, @@ -1511,8 +1546,8 @@ isort = [ {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] jsonrpcclient = [ {file = "jsonrpcclient-4.0.2.tar.gz", hash = "sha256:c0d475494b3e1b591ecdee7883739accaf5695edb673f16b7383b8c6bbdb1ca3"}, @@ -1626,8 +1661,8 @@ mkdocs-autorefs = [ {file = "mkdocs_autorefs-0.4.1-py3-none-any.whl", hash = "sha256:a2248a9501b29dc0cc8ba4c09f4f47ff121945f6ce33d760f145d6f89d313f5b"}, ] mkdocs-material = [ - {file = "mkdocs-material-8.2.7.tar.gz", hash = "sha256:3314d94ccc11481b1a3aa4f7babb4fb2bc47daa2fa8ace2463665952116f409b"}, - {file = "mkdocs_material-8.2.7-py2.py3-none-any.whl", hash = "sha256:20c13aa0a54841e1f1c080edb0e3573407884e4abea51ee25573061189bec83e"}, + {file = "mkdocs-material-8.2.6.tar.gz", hash = "sha256:be76ba3e0c0d4482159fc2c00d060dbf22cfb29f25276ebd0db9a0eaf6a18712"}, + {file = "mkdocs_material-8.2.6-py2.py3-none-any.whl", hash = "sha256:b30b4cfe5b0a74cccf2c75b7127c22cd8d816e6260c9a8708c3baf08c59e6714"}, ] mkdocs-material-extensions = [ {file = "mkdocs-material-extensions-1.0.3.tar.gz", hash = "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"}, @@ -1766,20 +1801,21 @@ pyrsistent = [ {file = "pyrsistent-0.18.1.tar.gz", hash = "sha256:d4d61f8b993a7255ba714df3aca52700f8125289f84f704cf80916517c46eb96"}, ] pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, - {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, + {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, + {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, + {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] pytest-docker = [ - {file = "pytest-docker-0.10.3.tar.gz", hash = "sha256:29a6a316508b0601f0085fbc45c9a54c7169f7213cac9ff906658186e3bcca23"}, - {file = "pytest_docker-0.10.3-py3-none-any.whl", hash = "sha256:292d23c5a1745aaa71b23edc24f410e9c4b1f52cb1ead859c923bd4438bac3b8"}, + {file = "pytest-docker-0.12.0.tar.gz", hash = "sha256:d9ae0d9cc41e0459931796f2fd7edb5e4fd859e2850d8530e0fd45b9259872e3"}, + {file = "pytest_docker-0.12.0-py3-none-any.whl", hash = "sha256:7d1e9f8ff46a137808760749c5072fc10d2b71286df92a04e5455a43a74de99e"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, diff --git a/pyproject.toml b/pyproject.toml index f17a3cbd..7ba33620 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,24 +30,24 @@ typing-extensions = ">=3.10.0" cachetools = "^4.2.2" types-cachetools = "^4.2.4" jsonrpcclient = "^4.0.1" -websockets = "^10.1" +websockets = "^10.3" jsonrpcserver = "^5.0.7" -apischema = "^0.16.1" +apischema = "^0.17.5" based58 = "^0.1.0" solders = "^0.2.0" [tool.poetry.dev-dependencies] black = "^22.3" -pytest = "^6.2.5" +pytest = "^7.1.2" pylint = "^2.11.1" mypy = "^0.910" pydocstyle = "^6.1.1" flake8 = "^4.0.1" isort = "^5.9.3" -pytest-docker = "^0.10.3" +pytest-docker = "^0.12.0" bump2version = "^1.0.1" types-requests = "^2.25.11" -pytest-asyncio = "^0.16.0" +pytest-asyncio = "^0.18.3" pytest-cov = "^3.0.0" asyncstdlib = "^3.10.2" mkdocstrings = "^0.18.0" diff --git a/pytest.ini b/pytest.ini index 3d031354..c5a3ab4c 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,5 @@ [pytest] addopts = --doctest-modules -p no:anyio +asyncio_mode = auto markers = integration: mark a test as a integration test. diff --git a/src/solana/rpc/api.py b/src/solana/rpc/api.py index 3639e6f2..30544932 100644 --- a/src/solana/rpc/api.py +++ b/src/solana/rpc/api.py @@ -7,12 +7,19 @@ from solana.blockhash import Blockhash, BlockhashCache from solana.keypair import Keypair +from solana.message import Message from solana.publickey import PublicKey from solana.rpc import types from solana.transaction import Transaction from .commitment import COMMITMENT_RANKS, Commitment, Finalized -from .core import RPCException, UnconfirmedTxError, _ClientCore +from .core import ( + RPCException, + TransactionExpiredBlockheightExceededError, + TransactionUncompiledError, + UnconfirmedTxError, + _ClientCore, +) from .providers import http @@ -621,6 +628,31 @@ def get_fee_calculator_for_blockhash( args = self._get_fee_calculator_for_blockhash_args(blockhash, commitment) return self._provider.make_request(*args) + def get_fee_for_message(self, message: Message, commitment: Optional[Commitment] = None) -> types.RPCResponse: + """Returns the fee for a message. + + Args: + message: Message that the fee is requested for. + commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". + + Example: + >>> from solana.keypair import Keypair + >>> from solana.system_program import TransferParams, transfer + >>> from solana.transaction import Transaction + >>> sender, receiver = Keypair.from_seed(bytes(PublicKey(1))), Keypair.from_seed(bytes(PublicKey(2))) + >>> txn = Transaction().add(transfer(TransferParams( + ... from_pubkey=sender.public_key, to_pubkey=receiver.public_key, lamports=1000))) + >>> solana_client = Client("http://localhost:8899") + >>> solana_client.get_fee_for_message(txn.compile_message()) # doctest: +SKIP + {'jsonrpc': '2.0', + 'result': { 'context': { 'slot': 5068 }, 'value': 5000 }, + 'id': 4} + """ # noqa: E501 # pylint: disable=line-too-long + if isinstance(message, Transaction): + raise TransactionUncompiledError("Transaction uncompiled, please compile to message first.") + args = self._get_fee_for_message_args(message, commitment) + return self._provider.make_request(*args) + def get_fee_rate_governor(self) -> types.RPCResponse: """Returns the fee rate governor information from the root bank. @@ -938,7 +970,7 @@ def get_recent_blockhash(self, commitment: Optional[Commitment] = None) -> types """Returns a recent block hash from the ledger. Response also includes a fee schedule that can be used to compute the cost - of submitting a transaction using it. + of submitting a transaction using it. Deprecated, please use get_latest_blockhash() instead. Args: commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". @@ -955,6 +987,26 @@ def get_recent_blockhash(self, commitment: Optional[Commitment] = None) -> types args = self._get_recent_blockhash_args(commitment) return self._provider.make_request(*args) + def get_latest_blockhash(self, commitment: Optional[Commitment] = None) -> types.RPCResponse: + """Returns the latest block hash from the ledger. + + Response also includes the last valid block height. + + Args: + commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". + + Example: + >>> solana_client = Client("http://localhost:8899") + >>> solana_client.get_latest_blockhash() # doctest: +SKIP + {'jsonrpc': '2.0', + 'result': {'context': {'slot': 1637}, + 'value': {'blockhash': 'EALChog1mXQ9nEgEUQpWAtmA5UueUZvZiL16ZivmR7eb', + 'lastValidBlockHeight': 3090}}, + 'id': 2} + """ + args = self._get_latest_blockhash_args(commitment) + return self._provider.make_request(*args) + def get_signature_statuses( self, signatures: List[Union[str, bytes]], search_transaction_history: bool = False ) -> types.RPCResponse: @@ -1308,23 +1360,33 @@ def send_transaction( 'result': '236zSA5w4NaVuLXXHK1mqiBuBxkNBu84X6cfLBh1v6zjPrLfyECz4zdedofBaZFhs4gdwzSmij9VkaSo2tR5LTgG', 'id': 12} """ - opts_to_use = types.TxOpts(preflight_commitment=self._commitment) if opts is None else opts + last_valid_block_height = None if recent_blockhash is None: if self.blockhash_cache: try: recent_blockhash = self.blockhash_cache.get() except ValueError: - blockhash_resp = self.get_recent_blockhash(Finalized) + blockhash_resp = self.get_latest_blockhash(Finalized) recent_blockhash = self._process_blockhash_resp(blockhash_resp, used_immediately=True) + last_valid_block_height = blockhash_resp["result"]["value"]["lastValidBlockHeight"] + else: - blockhash_resp = self.get_recent_blockhash(Finalized) + blockhash_resp = self.get_latest_blockhash(Finalized) recent_blockhash = self.parse_recent_blockhash(blockhash_resp) + last_valid_block_height = blockhash_resp["result"]["value"]["lastValidBlockHeight"] + txn.recent_blockhash = recent_blockhash txn.sign(*signers) + opts_to_use = ( + types.TxOpts(preflight_commitment=self._commitment, last_valid_block_height=last_valid_block_height) + if opts is None + else opts + ) + txn_resp = self.send_raw_transaction(txn.serialize(), opts=opts_to_use) if self.blockhash_cache: - blockhash_resp = self.get_recent_blockhash(Finalized) + blockhash_resp = self.get_latest_blockhash(Finalized) self._process_blockhash_resp(blockhash_resp, used_immediately=False) return txn_resp @@ -1383,16 +1445,22 @@ def validator_exit(self) -> types.RPCResponse: """ return self._provider.make_request(self._validator_exit) - def __post_send_with_confirm(self, resp: types.RPCResponse, conf_comm: Commitment) -> types.RPCResponse: + def __post_send_with_confirm( + self, resp: types.RPCResponse, conf_comm: Commitment, last_valid_block_height: Optional[int] + ) -> types.RPCResponse: resp = self._post_send(resp) self._provider.logger.info( "Transaction sent to %s. Signature %s: ", self._provider.endpoint_uri, resp["result"] ) - self.confirm_transaction(resp["result"], conf_comm) + self.confirm_transaction(resp["result"], conf_comm, last_valid_block_height=last_valid_block_height) return resp def confirm_transaction( - self, tx_sig: str, commitment: Optional[Commitment] = None, sleep_seconds: float = 0.5 + self, + tx_sig: str, + commitment: Optional[Commitment] = None, + sleep_seconds: float = 0.5, + last_valid_block_height: Optional[int] = None, ) -> types.RPCResponse: """Confirm the transaction identified by the specified signature. @@ -1400,25 +1468,48 @@ def confirm_transaction( tx_sig: the transaction signature to confirm. commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". sleep_seconds: The number of seconds to sleep when polling the signature status. + last_valid_block_height: The block height by which the transaction would become invalid. """ timeout = time() + 30 commitment_to_use = self._commitment if commitment is None else commitment commitment_rank = COMMITMENT_RANKS[commitment_to_use] - while time() < timeout: - resp = self.get_signature_statuses([tx_sig]) - maybe_rpc_error = resp.get("error") - if maybe_rpc_error is not None: - raise RPCException(maybe_rpc_error) - resp_value = resp["result"]["value"][0] - if resp_value is not None: - confirmation_status = resp_value["confirmationStatus"] - confirmation_rank = COMMITMENT_RANKS[confirmation_status] - if confirmation_rank >= commitment_rank: - break - sleep(sleep_seconds) + if last_valid_block_height: # pylint: disable=no-else-return + current_blockheight = (self.get_block_height(commitment))["result"] + while current_blockheight <= last_valid_block_height: + resp = self.get_signature_statuses([tx_sig]) + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + resp_value = resp["result"]["value"][0] + if resp_value is not None: + confirmation_status = resp_value["confirmationStatus"] + confirmation_rank = COMMITMENT_RANKS[confirmation_status] + if confirmation_rank >= commitment_rank: + break + current_blockheight = (self.get_block_height(commitment))["result"] + sleep(sleep_seconds) + else: + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + raise TransactionExpiredBlockheightExceededError(f"{tx_sig} has expired: block height exceeded") + return resp else: - maybe_rpc_error = resp.get("error") - if maybe_rpc_error is not None: - raise RPCException(maybe_rpc_error) - raise UnconfirmedTxError(f"Unable to confirm transaction {tx_sig}") - return resp + while time() < timeout: + resp = self.get_signature_statuses([tx_sig]) + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + resp_value = resp["result"]["value"][0] + if resp_value is not None: + confirmation_status = resp_value["confirmationStatus"] + confirmation_rank = COMMITMENT_RANKS[confirmation_status] + if confirmation_rank >= commitment_rank: + break + sleep(sleep_seconds) + else: + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + raise UnconfirmedTxError(f"Unable to confirm transaction {tx_sig}") + return resp diff --git a/src/solana/rpc/async_api.py b/src/solana/rpc/async_api.py index 13423bf8..1ffefc75 100644 --- a/src/solana/rpc/async_api.py +++ b/src/solana/rpc/async_api.py @@ -5,12 +5,19 @@ from solana.blockhash import Blockhash, BlockhashCache from solana.keypair import Keypair +from solana.message import Message from solana.publickey import PublicKey from solana.rpc import types from solana.transaction import Transaction from .commitment import COMMITMENT_RANKS, Commitment, Finalized -from .core import RPCException, UnconfirmedTxError, _ClientCore +from .core import ( + RPCException, + TransactionExpiredBlockheightExceededError, + TransactionUncompiledError, + UnconfirmedTxError, + _ClientCore, +) from .providers import async_http @@ -617,6 +624,31 @@ async def get_fee_calculator_for_blockhash( args = self._get_fee_calculator_for_blockhash_args(blockhash, commitment) return await self._provider.make_request(*args) + async def get_fee_for_message(self, message: Message, commitment: Optional[Commitment] = None) -> types.RPCResponse: + """Returns the fee for a message. + + Args: + message: Message that the fee is requested for. + commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". + + Example: + >>> from solana.keypair import Keypair + >>> from solana.system_program import TransferParams, transfer + >>> from solana.transaction import Transaction + >>> sender, receiver = Keypair.from_seed(bytes(PublicKey(1))), Keypair.from_seed(bytes(PublicKey(2))) + >>> txn = Transaction().add(transfer(TransferParams( + ... from_pubkey=sender.public_key, to_pubkey=receiver.public_key, lamports=1000))) + >>> solana_client = AsyncClient("http://localhost:8899") + >>> asyncio.run(solana_client.get_fee_for_message(txn.compile_message())) # doctest: +SKIP + {'jsonrpc': '2.0', + 'result': { 'context': { 'slot': 5068 }, 'value': 5000 }, + 'id': 4} + """ # noqa: E501 # pylint: disable=line-too-long + if isinstance(message, Transaction): + raise TransactionUncompiledError("Transaction uncompiled, please compile to message first.") + args = self._get_fee_for_message_args(message, commitment) + return await self._provider.make_request(*args) + async def get_fee_rate_governor(self) -> types.RPCResponse: """Return the fee rate governor information from the root bank. @@ -934,7 +966,7 @@ async def get_recent_blockhash(self, commitment: Optional[Commitment] = None) -> """Returns a recent block hash from the ledger. Response also includes a fee schedule that can be used to compute the cost - of submitting a transaction using it. + of submitting a transaction using it. Deprecated, please use get_latest_blockhash() instead. Args: commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". @@ -951,6 +983,26 @@ async def get_recent_blockhash(self, commitment: Optional[Commitment] = None) -> args = self._get_recent_blockhash_args(commitment) return await self._provider.make_request(*args) + async def get_latest_blockhash(self, commitment: Optional[Commitment] = None) -> types.RPCResponse: + """Returns the latest block hash from the ledger. + + Response also includes the last valid block height. + + Args: + commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". + + Example: + >>> solana_client = AsyncClient("http://localhost:8899") + >>> asyncio.run(solana_client.get_latest_blockhash()) # doctest: +SKIP + {'jsonrpc': '2.0', + 'result': {'context': {'slot': 1637}, + 'value': {'blockhash': 'EALChog1mXQ9nEgEUQpWAtmA5UueUZvZiL16ZivmR7eb', + 'lastValidBlockHeight': 3090}}, + 'id': 2} + """ + args = self._get_latest_blockhash_args(commitment) + return await self._provider.make_request(*args) + async def get_signature_statuses( self, signatures: List[Union[str, bytes]], search_transaction_history: bool = False ) -> types.RPCResponse: @@ -1306,23 +1358,31 @@ async def send_transaction( 'result': '236zSA5w4NaVuLXXHK1mqiBuBxkNBu84X6cfLBh1v6zjPrLfyECz4zdedofBaZFhs4gdwzSmij9VkaSo2tR5LTgG', 'id': 12} """ + last_valid_block_height = None if recent_blockhash is None: if self.blockhash_cache: try: recent_blockhash = self.blockhash_cache.get() except ValueError: - blockhash_resp = await self.get_recent_blockhash(Finalized) + blockhash_resp = await self.get_latest_blockhash(Finalized) recent_blockhash = self._process_blockhash_resp(blockhash_resp, used_immediately=True) + last_valid_block_height = blockhash_resp["result"]["value"]["lastValidBlockHeight"] else: - blockhash_resp = await self.get_recent_blockhash(Finalized) + blockhash_resp = await self.get_latest_blockhash(Finalized) recent_blockhash = self.parse_recent_blockhash(blockhash_resp) + last_valid_block_height = blockhash_resp["result"]["value"]["lastValidBlockHeight"] + txn.recent_blockhash = recent_blockhash txn.sign(*signers) - opts_to_use = types.TxOpts(preflight_commitment=self._commitment) if opts is None else opts + opts_to_use = ( + types.TxOpts(preflight_commitment=self._commitment, last_valid_block_height=last_valid_block_height) + if opts is None + else opts + ) txn_resp = await self.send_raw_transaction(txn.serialize(), opts=opts_to_use) if self.blockhash_cache: - blockhash_resp = await self.get_recent_blockhash(Finalized) + blockhash_resp = await self.get_latest_blockhash(Finalized) self._process_blockhash_resp(blockhash_resp, used_immediately=False) return txn_resp @@ -1381,16 +1441,22 @@ async def validator_exit(self) -> types.RPCResponse: """ return await self._provider.make_request(self._validator_exit) - async def __post_send_with_confirm(self, resp: types.RPCResponse, conf_comm: Commitment) -> types.RPCResponse: + async def __post_send_with_confirm( + self, resp: types.RPCResponse, conf_comm: Commitment, last_valid_block_height: Optional[int] + ) -> types.RPCResponse: resp = self._post_send(resp) self._provider.logger.info( "Transaction sent to %s. Signature %s: ", self._provider.endpoint_uri, resp["result"] ) - await self.confirm_transaction(resp["result"], conf_comm) + await self.confirm_transaction(resp["result"], conf_comm, last_valid_block_height=last_valid_block_height) return resp async def confirm_transaction( - self, tx_sig: str, commitment: Optional[Commitment] = None, sleep_seconds: float = 0.5 + self, + tx_sig: str, + commitment: Optional[Commitment] = None, + sleep_seconds: float = 0.5, + last_valid_block_height: Optional[int] = None, ) -> types.RPCResponse: """Confirm the transaction identified by the specified signature. @@ -1398,25 +1464,48 @@ async def confirm_transaction( tx_sig: the transaction signature to confirm. commitment: Bank state to query. It can be either "finalized", "confirmed" or "processed". sleep_seconds: The number of seconds to sleep when polling the signature status. + last_valid_block_height: The block height by which the transaction would become invalid. """ - timeout = time() + 30 commitment_to_use = self._commitment if commitment is None else commitment commitment_rank = COMMITMENT_RANKS[commitment_to_use] - while time() < timeout: - resp = await self.get_signature_statuses([tx_sig]) - maybe_rpc_error = resp.get("error") - if maybe_rpc_error is not None: - raise RPCException(maybe_rpc_error) - resp_value = resp["result"]["value"][0] - if resp_value is not None: - confirmation_status = resp_value["confirmationStatus"] - confirmation_rank = COMMITMENT_RANKS[confirmation_status] - if confirmation_rank >= commitment_rank: - break - await asyncio.sleep(sleep_seconds) + if last_valid_block_height: # pylint: disable=no-else-return + current_blockheight = (await self.get_block_height(commitment))["result"] + while current_blockheight <= last_valid_block_height: + resp = await self.get_signature_statuses([tx_sig]) + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + resp_value = resp["result"]["value"][0] + if resp_value is not None: + confirmation_status = resp_value["confirmationStatus"] + confirmation_rank = COMMITMENT_RANKS[confirmation_status] + if confirmation_rank >= commitment_rank: + break + current_blockheight = (await self.get_block_height(commitment))["result"] + await asyncio.sleep(sleep_seconds) + else: + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + raise TransactionExpiredBlockheightExceededError(f"{tx_sig} has expired: block height exceeded") + return resp else: - maybe_rpc_error = resp.get("error") - if maybe_rpc_error is not None: - raise RPCException(maybe_rpc_error) - raise UnconfirmedTxError(f"Unable to confirm transaction {tx_sig}") - return resp + timeout = time() + 30 + while time() < timeout: + resp = await self.get_signature_statuses([tx_sig]) + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + resp_value = resp["result"]["value"][0] + if resp_value is not None: + confirmation_status = resp_value["confirmationStatus"] + confirmation_rank = COMMITMENT_RANKS[confirmation_status] + if confirmation_rank >= commitment_rank: + break + await asyncio.sleep(sleep_seconds) + else: + maybe_rpc_error = resp.get("error") + if maybe_rpc_error is not None: + raise RPCException(maybe_rpc_error) + raise UnconfirmedTxError(f"Unable to confirm transaction {tx_sig}") + return resp diff --git a/src/solana/rpc/core.py b/src/solana/rpc/core.py index 436acecb..a0308f26 100644 --- a/src/solana/rpc/core.py +++ b/src/solana/rpc/core.py @@ -14,6 +14,7 @@ from solana.blockhash import Blockhash, BlockhashCache from solana.keypair import Keypair +from solana.message import Message from solana.publickey import PublicKey from solana.rpc import types from solana.transaction import Transaction @@ -33,6 +34,14 @@ class UnconfirmedTxError(Exception): """Raise when confirming a transaction times out.""" +class TransactionExpiredBlockheightExceededError(Exception): + """Raise when confirming an expired transaction that exceeded the blockheight.""" + + +class TransactionUncompiledError(Exception): + """Raise when transaction is not compiled to a message.""" + + class _ClientCore: # pylint: disable=too-few-public-methods _comm_key = "commitment" _encoding_key = "encoding" @@ -200,6 +209,16 @@ def _get_fee_calculator_for_blockhash_args( {self._comm_key: commitment or self._commitment}, ) + def _get_fee_for_message_args( + self, message: Message, commitment: Optional[Commitment] + ) -> Tuple[types.RPCMethod, str, Dict[str, Commitment]]: + raw_message = b64encode(message.serialize()).decode("utf-8") + return ( + types.RPCMethod("getFeeForMessage"), + raw_message, + {self._comm_key: commitment or self._commitment}, + ) + def _get_fees_args(self, commitment: Optional[Commitment]) -> Tuple[types.RPCMethod, Dict[str, Commitment]]: return types.RPCMethod("getFees"), {self._comm_key: commitment or self._commitment} @@ -268,6 +287,11 @@ def _get_recent_blockhash_args( ) -> Tuple[types.RPCMethod, Dict[str, Commitment]]: return types.RPCMethod("getRecentBlockhash"), {self._comm_key: commitment or self._commitment} + def _get_latest_blockhash_args( + self, commitment: Optional[Commitment] + ) -> Tuple[types.RPCMethod, Dict[str, Commitment]]: + return types.RPCMethod("getLatestBlockhash"), {self._comm_key: commitment or self._commitment} + @staticmethod def _get_signature_statuses_args( signatures: List[Union[str, bytes]], search_transaction_history: bool @@ -395,8 +419,8 @@ def _send_raw_transaction_args( @staticmethod def _send_raw_transaction_post_send_args( resp: types.RPCResponse, opts: types.TxOpts - ) -> Tuple[types.RPCResponse, Commitment]: - return resp, opts.preflight_commitment + ) -> Tuple[types.RPCResponse, Commitment, Optional[int]]: + return resp, opts.preflight_commitment, opts.last_valid_block_height def _simulate_transaction_args( self, txn: Union[bytes, str, Transaction], sig_verify: bool, commitment: Optional[Commitment] diff --git a/src/solana/rpc/responses.py b/src/solana/rpc/responses.py index c7c733d1..585af2e2 100644 --- a/src/solana/rpc/responses.py +++ b/src/solana/rpc/responses.py @@ -241,6 +241,7 @@ class VoteItem: hash: str slots: List[int] timestamp: Optional[int] + signature: str @dataclass diff --git a/src/solana/rpc/types.py b/src/solana/rpc/types.py index 1da6a2e9..44b36b70 100644 --- a/src/solana/rpc/types.py +++ b/src/solana/rpc/types.py @@ -92,5 +92,9 @@ class TxOpts(NamedTuple): max_retries: Optional[int] = None """Maximum number of times for the RPC node to retry sending the transaction to the leader. If this parameter not provided, the RPC node will retry the transaction until it is finalized - or until the blockhash expires + or until the blockhash expires. + """ + last_valid_block_height: Optional[int] = None + """Pass the latest valid block height here, to be consumed by confirm_transaction. + Valid only if skip_confirmation is False. """ diff --git a/tests/conftest.py b/tests/conftest.py index 0444c589..fba80448 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,10 @@ class Clients(NamedTuple): @pytest.fixture(scope="session") def event_loop(): """Event loop for pytest-asyncio.""" - loop = asyncio.get_event_loop() + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() yield loop loop.close() diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 6bb57e12..90fad39a 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: localnet: - image: "solanalabs/solana:v1.9.14" + image: "solanalabs/solana:v1.10.25" ports: - "8899:8899" - "8900:8900" diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index cd5b0a84..57d7b838 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -6,9 +6,9 @@ from solana.publickey import PublicKey from solana.rpc.api import DataSliceOpt from solana.rpc.async_api import AsyncClient -from solana.rpc.commitment import Finalized -from solana.rpc.core import RPCException -from solana.rpc.types import RPCError +from solana.rpc.commitment import Finalized, Processed +from solana.rpc.core import RPCException, TransactionExpiredBlockheightExceededError, TransactionUncompiledError +from solana.rpc.types import RPCError, TxOpts from solana.transaction import Transaction from spl.token.constants import WRAPPED_SOL_MINT @@ -16,7 +16,6 @@ @pytest.mark.integration -@pytest.mark.asyncio async def test_request_air_drop( async_stubbed_sender: Keypair, async_stubbed_receiver: PublicKey, test_http_client_async: AsyncClient ): @@ -36,7 +35,6 @@ async def test_request_air_drop( @pytest.mark.integration -@pytest.mark.asyncio async def test_request_air_drop_prefetched_blockhash( async_stubbed_sender_prefetched_blockhash, async_stubbed_receiver_prefetched_blockhash, test_http_client_async ): @@ -58,7 +56,6 @@ async def test_request_air_drop_prefetched_blockhash( @pytest.mark.integration -@pytest.mark.asyncio async def test_request_air_drop_cached_blockhash( async_stubbed_sender_cached_blockhash, async_stubbed_receiver_cached_blockhash, @@ -86,7 +83,6 @@ async def test_request_air_drop_cached_blockhash( @pytest.mark.integration -@pytest.mark.asyncio async def test_send_invalid_transaction(test_http_client_async): """Test sending an invalid transaction to localnet.""" # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver @@ -96,7 +92,6 @@ async def test_send_invalid_transaction(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_send_transaction_and_get_balance(async_stubbed_sender, async_stubbed_receiver, test_http_client_async): """Test sending a transaction to localnet.""" # Create transfer tx to transfer lamports from stubbed sender to async_stubbed_receiver @@ -121,7 +116,6 @@ async def test_send_transaction_and_get_balance(async_stubbed_sender, async_stub @pytest.mark.integration -@pytest.mark.asyncio async def test_send_transaction_prefetched_blockhash( async_stubbed_sender_prefetched_blockhash, async_stubbed_receiver_prefetched_blockhash, test_http_client_async ): @@ -150,7 +144,6 @@ async def test_send_transaction_prefetched_blockhash( @pytest.mark.integration -@pytest.mark.asyncio async def test_send_transaction_cached_blockhash( async_stubbed_sender_cached_blockhash, async_stubbed_receiver_cached_blockhash, @@ -212,7 +205,6 @@ async def test_send_transaction_cached_blockhash( @pytest.mark.integration -@pytest.mark.asyncio async def test_send_raw_transaction_and_get_balance( async_stubbed_sender, async_stubbed_receiver, test_http_client_async ): @@ -246,7 +238,45 @@ async def test_send_raw_transaction_and_get_balance( @pytest.mark.integration -@pytest.mark.asyncio +async def test_send_raw_transaction_and_get_balance_using_latest_blockheight( + async_stubbed_sender, async_stubbed_receiver, test_http_client_async +): + """Test sending a raw transaction to localnet using latest blockhash.""" + # Get latest blockhash + resp = await test_http_client_async.get_latest_blockhash(Finalized) + assert_valid_response(resp) + recent_blockhash = resp["result"]["value"]["blockhash"] + last_valid_block_height = resp["result"]["value"]["lastValidBlockHeight"] + # Create transfer tx transfer lamports from stubbed sender to async_stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer( + sp.TransferParams( + from_pubkey=async_stubbed_sender.public_key, to_pubkey=async_stubbed_receiver, lamports=1000 + ) + ) + ) + # Sign transaction + transfer_tx.sign(async_stubbed_sender) + # Send raw transaction + resp = await test_http_client_async.send_raw_transaction( + transfer_tx.serialize(), + opts=TxOpts(preflight_commitment=Processed, last_valid_block_height=last_valid_block_height), + ) + assert_valid_response(resp) + # Confirm transaction + resp = await test_http_client_async.confirm_transaction( + resp["result"], last_valid_block_height=last_valid_block_height + ) + # Check balances + resp = await test_http_client_async.get_balance(async_stubbed_sender.public_key) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999982000 + resp = await test_http_client_async.get_balance(async_stubbed_receiver) + assert_valid_response(resp) + assert resp["result"]["value"] == 10000003000 + + +@pytest.mark.integration async def test_confirm_bad_signature(test_http_client_async: AsyncClient) -> None: """Test that RPCException is raised when trying to confirm an invalid signature.""" with pytest.raises(RPCException) as exc_info: @@ -256,7 +286,66 @@ async def test_confirm_bad_signature(test_http_client_async: AsyncClient) -> Non @pytest.mark.integration -@pytest.mark.asyncio +async def test_confirm_expired_transaction(stubbed_sender, stubbed_receiver, test_http_client_async): + """Test that RPCException is raised when trying to confirm a transaction that exceeded last valid block height.""" + # Get a recent blockhash + resp = await test_http_client_async.get_latest_blockhash() + recent_blockhash = resp["result"]["value"]["blockhash"] + last_valid_block_height = resp["result"]["value"]["lastValidBlockHeight"] - 330 + # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000)) + ) + # Sign transaction + transfer_tx.sign(stubbed_sender) + # Send raw transaction + resp = await test_http_client_async.send_raw_transaction( + transfer_tx.serialize(), opts=TxOpts(skip_confirmation=True, skip_preflight=True) + ) + assert_valid_response(resp) + # Confirm transaction + with pytest.raises(TransactionExpiredBlockheightExceededError) as exc_info: + await test_http_client_async.confirm_transaction( + resp["result"], Finalized, last_valid_block_height=last_valid_block_height + ) + err_object = exc_info.value.args[0] + assert "block height exceeded" in err_object + + +@pytest.mark.integration +async def test_get_fee_for_transaction_message(stubbed_sender, stubbed_receiver, test_http_client_async): + """Test that gets a fee for a transaction using get fee for message.""" + # Get latest blockhash + resp = await test_http_client_async.get_latest_blockhash() + recent_blockhash = resp["result"]["value"]["blockhash"] + # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000)) + ) + # Get fee for transaction message + resp = await test_http_client_async.get_fee_for_message(transfer_tx.compile_message()) + assert_valid_response(resp) + assert resp["result"]["value"] is not None + + +@pytest.mark.integration +async def test_get_fee_for_uncompiled_transaction_message(stubbed_sender, stubbed_receiver, test_http_client_async): + """Test that gets a fee for a transaction that is uncompiled using get fee for message.""" + # Get latest blockhash + resp = await test_http_client_async.get_latest_blockhash() + recent_blockhash = resp["result"]["value"]["blockhash"] + # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000)) + ) + # fails when transaction has not been compiled via .compile_message() + with pytest.raises(TransactionUncompiledError) as exc_info: + resp = await test_http_client_async.get_fee_for_message(transfer_tx) + err_object = exc_info.value.args[0] + assert "Transaction uncompiled" in err_object + + +@pytest.mark.integration async def test_get_block_commitment(test_http_client_async): """Test get block commitment.""" resp = await test_http_client_async.get_block_commitment(5) @@ -264,7 +353,6 @@ async def test_get_block_commitment(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_block_time(test_http_client_async): """Test get block time.""" resp = await test_http_client_async.get_block_time(5) @@ -272,7 +360,6 @@ async def test_get_block_time(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_cluster_nodes(test_http_client_async): """Test get cluster nodes.""" resp = await test_http_client_async.get_cluster_nodes() @@ -280,7 +367,6 @@ async def test_get_cluster_nodes(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_confirmed_block(test_http_client_async): """Test get confirmed block.""" resp = await test_http_client_async.get_confirmed_block(2) @@ -288,7 +374,6 @@ async def test_get_confirmed_block(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_confirmed_block_with_encoding(test_http_client_async): """Test get confrimed block with encoding.""" resp = await test_http_client_async.get_confirmed_block(2, encoding="base64") @@ -296,7 +381,6 @@ async def test_get_confirmed_block_with_encoding(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_block(test_http_client_async): """Test get block.""" resp = await test_http_client_async.get_block(2) @@ -304,7 +388,6 @@ async def test_get_block(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_block_height(test_http_client_async): """Test get height.""" resp = await test_http_client_async.get_block_height() @@ -312,7 +395,6 @@ async def test_get_block_height(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_block_with_encoding(test_http_client_async): """Test get block with encoding.""" resp = await test_http_client_async.get_block(2, encoding="base64") @@ -320,7 +402,6 @@ async def test_get_block_with_encoding(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_confirmed_blocks(test_http_client_async): """Test get confirmed blocks.""" resp = await test_http_client_async.get_confirmed_blocks(5, 10) @@ -328,7 +409,6 @@ async def test_get_confirmed_blocks(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_blocks(test_http_client_async): """Test get blocks.""" resp = await test_http_client_async.get_blocks(5, 10) @@ -336,7 +416,6 @@ async def test_get_blocks(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_confirmed_signature_for_address2(test_http_client_async): """Test get confirmed signature for address2.""" resp = await test_http_client_async.get_confirmed_signature_for_address2( @@ -346,7 +425,6 @@ async def test_get_confirmed_signature_for_address2(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_signatures_for_address(test_http_client_async): """Test get signatures for addresses.""" resp = await test_http_client_async.get_signatures_for_address( @@ -356,7 +434,6 @@ async def test_get_signatures_for_address(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_epoch_info(test_http_client_async): """Test get epoch info.""" resp = await test_http_client_async.get_epoch_info() @@ -364,7 +441,6 @@ async def test_get_epoch_info(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_epoch_schedule(test_http_client_async): """Test get epoch schedule.""" resp = await test_http_client_async.get_epoch_schedule() @@ -372,7 +448,6 @@ async def test_get_epoch_schedule(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_fee_calculator_for_blockhash(test_http_client_async): """Test get fee calculator for blockhash.""" resp = await test_http_client_async.get_recent_blockhash(Finalized) @@ -382,7 +457,15 @@ async def test_get_fee_calculator_for_blockhash(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio +async def test_get_latest_blockhash(test_http_client_async): + """Test get latest blockhash.""" + resp = await test_http_client_async.get_latest_blockhash(Finalized) + assert_valid_response(resp) + assert resp["result"]["value"]["blockhash"] is not None + assert resp["result"]["value"]["lastValidBlockHeight"] is not None + + +@pytest.mark.integration async def test_get_slot(test_http_client_async): """Test get slot.""" resp = await test_http_client_async.get_slot() @@ -390,7 +473,6 @@ async def test_get_slot(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_fees(test_http_client_async): """Test get fees.""" resp = await test_http_client_async.get_fees() @@ -398,7 +480,6 @@ async def test_get_fees(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_first_available_block(test_http_client_async): """Test get first available block.""" resp = await test_http_client_async.get_first_available_block() @@ -406,7 +487,6 @@ async def test_get_first_available_block(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_genesis_hash(test_http_client_async): """Test get genesis hash.""" resp = await test_http_client_async.get_genesis_hash() @@ -414,7 +494,6 @@ async def test_get_genesis_hash(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_identity(test_http_client_async): """Test get identity.""" resp = await test_http_client_async.get_genesis_hash() @@ -422,7 +501,6 @@ async def test_get_identity(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_inflation_governor(test_http_client_async): """Test get inflation governor.""" resp = await test_http_client_async.get_inflation_governor() @@ -430,7 +508,6 @@ async def test_get_inflation_governor(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_inflation_rate(test_http_client_async): """Test get inflation rate.""" resp = await test_http_client_async.get_inflation_rate() @@ -438,7 +515,6 @@ async def test_get_inflation_rate(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_largest_accounts(test_http_client_async): """Test get largest accounts.""" resp = await test_http_client_async.get_largest_accounts() @@ -446,7 +522,6 @@ async def test_get_largest_accounts(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_leader_schedule(test_http_client_async): """Test get leader schedule.""" resp = await test_http_client_async.get_leader_schedule() @@ -454,7 +529,6 @@ async def test_get_leader_schedule(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_minimum_balance_for_rent_exemption(test_http_client_async): """Test get minimum balance for rent exemption.""" resp = await test_http_client_async.get_minimum_balance_for_rent_exemption(50) @@ -462,7 +536,6 @@ async def test_get_minimum_balance_for_rent_exemption(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_slot_leader(test_http_client_async): """Test get slot leader.""" resp = await test_http_client_async.get_slot_leader() @@ -470,7 +543,6 @@ async def test_get_slot_leader(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_supply(test_http_client_async): """Test get slot leader.""" resp = await test_http_client_async.get_supply() @@ -478,7 +550,6 @@ async def test_get_supply(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_token_largest_accounts(test_http_client_async): """Test get token largest accounts.""" resp = await test_http_client_async.get_token_largest_accounts(WRAPPED_SOL_MINT) @@ -486,7 +557,6 @@ async def test_get_token_largest_accounts(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_token_supply(test_http_client_async): """Test get token supply.""" resp = await test_http_client_async.get_token_supply(WRAPPED_SOL_MINT) @@ -494,7 +564,6 @@ async def test_get_token_supply(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_transaction_count(test_http_client_async): """Test get transactinon count.""" resp = await test_http_client_async.get_transaction_count() @@ -502,7 +571,6 @@ async def test_get_transaction_count(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_version(test_http_client_async): """Test get version.""" resp = await test_http_client_async.get_version() @@ -510,7 +578,6 @@ async def test_get_version(test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_account_info(async_stubbed_sender, test_http_client_async): """Test get_account_info.""" resp = await test_http_client_async.get_account_info(async_stubbed_sender.public_key) @@ -522,7 +589,6 @@ async def test_get_account_info(async_stubbed_sender, test_http_client_async): @pytest.mark.integration -@pytest.mark.asyncio async def test_get_multiple_accounts(async_stubbed_sender, test_http_client_async): """Test get_multiple_accounts.""" pubkeys = [async_stubbed_sender.public_key] * 2 @@ -535,7 +601,6 @@ async def test_get_multiple_accounts(async_stubbed_sender, test_http_client_asyn @pytest.mark.integration -@pytest.mark.asyncio async def test_get_vote_accounts(test_http_client_async): """Test get vote accounts.""" resp = await test_http_client_async.get_vote_accounts() diff --git a/tests/integration/test_async_token_client.py b/tests/integration/test_async_token_client.py index 43839a79..a77e7eb5 100644 --- a/tests/integration/test_async_token_client.py +++ b/tests/integration/test_async_token_client.py @@ -4,20 +4,20 @@ import spl.token._layouts as layouts from solana.publickey import PublicKey +from solana.rpc.commitment import Finalized from solana.utils.helpers import decode_byte_string from spl.token.async_client import AsyncToken from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID -from .utils import AIRDROP_AMOUNT, assert_valid_response, OPTS +from .utils import AIRDROP_AMOUNT, OPTS, assert_valid_response @pytest.mark.integration -@pytest.mark.asyncio @pytest.fixture(scope="module") async def test_token(stubbed_sender, freeze_authority, test_http_client_async) -> AsyncToken: """Test create mint.""" resp = await test_http_client_async.request_airdrop(stubbed_sender.public_key, AIRDROP_AMOUNT) - await test_http_client_async.confirm_transaction(resp["result"]) + await test_http_client_async.confirm_transaction(resp["result"], commitment=Finalized) assert_valid_response(resp) expected_decimals = 6 @@ -48,7 +48,6 @@ async def test_token(stubbed_sender, freeze_authority, test_http_client_async) - @pytest.mark.integration -@pytest.mark.asyncio @pytest.fixture(scope="module") async def stubbed_sender_token_account_pk( stubbed_sender, test_token # pylint: disable=redefined-outer-name @@ -58,7 +57,6 @@ async def stubbed_sender_token_account_pk( @pytest.mark.integration -@pytest.mark.asyncio @pytest.fixture(scope="module") async def async_stubbed_receiver_token_account_pk( async_stubbed_receiver, test_token # pylint: disable=redefined-outer-name @@ -68,7 +66,6 @@ async def async_stubbed_receiver_token_account_pk( @pytest.mark.integration -@pytest.mark.asyncio async def test_new_account(stubbed_sender, test_http_client_async, test_token): # pylint: disable=redefined-outer-name """Test creating a new token account.""" token_account_pk = await test_token.create_account(stubbed_sender.public_key) @@ -91,7 +88,6 @@ async def test_new_account(stubbed_sender, test_http_client_async, test_token): @pytest.mark.integration -@pytest.mark.asyncio async def test_new_associated_account(test_token): # pylint: disable=redefined-outer-name """Test creating a new associated token account.""" new_acct = PublicKey(0) @@ -104,7 +100,6 @@ async def test_new_associated_account(test_token): # pylint: disable=redefined- @pytest.mark.integration -@pytest.mark.asyncio async def test_get_account_info( stubbed_sender, stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name @@ -123,7 +118,6 @@ async def test_get_account_info( @pytest.mark.integration -@pytest.mark.asyncio async def test_get_mint_info(stubbed_sender, freeze_authority, test_token): # pylint: disable=redefined-outer-name """Test get token mint info.""" mint_info = await test_token.get_mint_info() @@ -135,7 +129,6 @@ async def test_get_mint_info(stubbed_sender, freeze_authority, test_token): # p @pytest.mark.integration -@pytest.mark.asyncio async def test_mint_to( stubbed_sender, stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name @@ -156,7 +149,6 @@ async def test_mint_to( @pytest.mark.integration -@pytest.mark.asyncio async def test_transfer( stubbed_sender, async_stubbed_receiver_token_account_pk, stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name @@ -178,7 +170,6 @@ async def test_transfer( @pytest.mark.integration -@pytest.mark.asyncio async def test_burn( stubbed_sender, stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name @@ -203,7 +194,6 @@ async def test_burn( @pytest.mark.integration -@pytest.mark.asyncio async def test_mint_to_checked( stubbed_sender, stubbed_sender_token_account_pk, @@ -232,7 +222,6 @@ async def test_mint_to_checked( @pytest.mark.integration -@pytest.mark.asyncio async def test_transfer_checked( stubbed_sender, async_stubbed_receiver_token_account_pk, stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name @@ -260,7 +249,6 @@ async def test_transfer_checked( @pytest.mark.integration -@pytest.mark.asyncio async def test_burn_checked( stubbed_sender, stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name @@ -286,7 +274,6 @@ async def test_burn_checked( @pytest.mark.integration -@pytest.mark.asyncio async def test_get_accounts(stubbed_sender, test_token): # pylint: disable=redefined-outer-name """Test get token accounts.""" resp = await test_token.get_accounts(stubbed_sender.public_key) @@ -299,7 +286,6 @@ async def test_get_accounts(stubbed_sender, test_token): # pylint: disable=rede @pytest.mark.integration -@pytest.mark.asyncio async def test_approve( stubbed_sender, async_stubbed_receiver, @@ -324,7 +310,6 @@ async def test_approve( @pytest.mark.integration -@pytest.mark.asyncio async def test_revoke( stubbed_sender, async_stubbed_receiver, @@ -349,7 +334,6 @@ async def test_revoke( @pytest.mark.integration -@pytest.mark.asyncio async def test_approve_checked( stubbed_sender, async_stubbed_receiver, @@ -375,7 +359,6 @@ async def test_approve_checked( @pytest.mark.integration -@pytest.mark.asyncio async def test_freeze_account( stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async ): # pylint: disable=redefined-outer-name @@ -395,7 +378,6 @@ async def test_freeze_account( @pytest.mark.integration -@pytest.mark.asyncio async def test_thaw_account( stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async ): # pylint: disable=redefined-outer-name @@ -411,7 +393,6 @@ async def test_thaw_account( @pytest.mark.integration -@pytest.mark.asyncio async def test_close_account( stubbed_sender, stubbed_sender_token_account_pk, @@ -439,7 +420,6 @@ async def test_close_account( @pytest.mark.integration -@pytest.mark.asyncio async def test_create_multisig( stubbed_sender, async_stubbed_receiver, test_token, test_http_client ): # pylint: disable=redefined-outer-name diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index 3f219a63..93247e09 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -5,8 +5,9 @@ from solana.keypair import Keypair from solana.publickey import PublicKey from solana.rpc.api import Client, DataSliceOpt -from solana.rpc.core import RPCException -from solana.rpc.types import RPCError +from solana.rpc.commitment import Finalized, Processed +from solana.rpc.core import RPCException, TransactionExpiredBlockheightExceededError, TransactionUncompiledError +from solana.rpc.types import RPCError, TxOpts from solana.transaction import Transaction from spl.token.constants import WRAPPED_SOL_MINT @@ -214,6 +215,39 @@ def test_send_raw_transaction_and_get_balance(stubbed_sender, stubbed_receiver, assert resp["result"]["value"] == 10000002000 +@pytest.mark.integration +def test_send_raw_transaction_and_get_balance_using_latest_blockheight( + stubbed_sender, stubbed_receiver, test_http_client +): + """Test sending a raw transaction to localnet using latest blockhash.""" + # Get a recent blockhash + resp = test_http_client.get_latest_blockhash(Finalized) + assert_valid_response(resp) + recent_blockhash = resp["result"]["value"]["blockhash"] + last_valid_block_height = resp["result"]["value"]["lastValidBlockHeight"] + # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000)) + ) + # Sign transaction + transfer_tx.sign(stubbed_sender) + # Send raw transaction + resp = test_http_client.send_raw_transaction( + transfer_tx.serialize(), + opts=TxOpts(preflight_commitment=Processed, last_valid_block_height=last_valid_block_height), + ) + assert_valid_response(resp) + # Confirm transaction + test_http_client.confirm_transaction(resp["result"], last_valid_block_height=last_valid_block_height) + # Check balances + resp = test_http_client.get_balance(stubbed_sender.public_key) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999982000 + resp = test_http_client.get_balance(stubbed_receiver) + assert_valid_response(resp) + assert resp["result"]["value"] == 10000003000 + + @pytest.mark.integration def test_confirm_bad_signature(test_http_client: Client) -> None: """Test that RPCException is raised when trying to confirm an invalid signature.""" @@ -223,6 +257,64 @@ def test_confirm_bad_signature(test_http_client: Client) -> None: assert err_object == {"code": -32602, "message": "Invalid param: WrongSize"} +@pytest.mark.integration +def test_confirm_expired_transaction(stubbed_sender, stubbed_receiver, test_http_client): + """Test that RPCException is raised when trying to confirm a transaction that exceeded last valid block height.""" + # Get a recent blockhash + resp = test_http_client.get_latest_blockhash() + recent_blockhash = resp["result"]["value"]["blockhash"] + last_valid_block_height = resp["result"]["value"]["lastValidBlockHeight"] - 330 + # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000)) + ) + # Sign transaction + transfer_tx.sign(stubbed_sender) + # Send raw transaction + resp = test_http_client.send_raw_transaction( + transfer_tx.serialize(), opts=TxOpts(skip_confirmation=True, skip_preflight=True) + ) + assert_valid_response(resp) + # Confirm transaction + with pytest.raises(TransactionExpiredBlockheightExceededError) as exc_info: + test_http_client.confirm_transaction(resp["result"], Finalized, last_valid_block_height=last_valid_block_height) + err_object = exc_info.value.args[0] + assert "block height exceeded" in err_object + + +@pytest.mark.integration +def test_get_fee_for_transaction(stubbed_sender, stubbed_receiver, test_http_client): + """Test that gets a fee for a transaction using get_fee_for_message.""" + # Get a recent blockhash + resp = test_http_client.get_latest_blockhash() + recent_blockhash = resp["result"]["value"]["blockhash"] + # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000)) + ) + # get fee for transaction + resp = test_http_client.get_fee_for_message(transfer_tx.compile_message()) + assert_valid_response(resp) + assert resp["result"]["value"] is not None + + +@pytest.mark.integration +def test_get_fee_for_uncompiled_transaction(stubbed_sender, stubbed_receiver, test_http_client): + """Test that gets a fee for a transaction using get_fee_for_message.""" + # Get a recent blockhash + resp = test_http_client.get_latest_blockhash() + recent_blockhash = resp["result"]["value"]["blockhash"] + # Create transfer tx transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction(recent_blockhash=recent_blockhash).add( + sp.transfer(sp.TransferParams(from_pubkey=stubbed_sender.public_key, to_pubkey=stubbed_receiver, lamports=1000)) + ) + # Get fee for transaction + with pytest.raises(TransactionUncompiledError) as exc_info: + resp = test_http_client.get_fee_for_message(transfer_tx) + err_object = exc_info.value.args[0] + assert "Transaction uncompiled" in err_object + + @pytest.mark.integration def test_get_block_commitment(test_http_client): """Test get block commitment.""" @@ -330,6 +422,15 @@ def test_get_fee_calculator_for_blockhash(test_http_client): assert_valid_response(resp) +@pytest.mark.integration +def test_get_latest_blockhash(test_http_client): + """Test get latest blockhash.""" + resp = test_http_client.get_latest_blockhash() + assert_valid_response(resp) + assert resp["result"]["value"]["blockhash"] is not None + assert resp["result"]["value"]["lastValidBlockHeight"] is not None + + @pytest.mark.integration def test_get_slot(test_http_client): """Test get slot.""" diff --git a/tests/integration/test_memo.py b/tests/integration/test_memo.py index 36c14799..49ac46ba 100644 --- a/tests/integration/test_memo.py +++ b/tests/integration/test_memo.py @@ -8,12 +8,15 @@ from spl.memo.constants import MEMO_PROGRAM_ID from spl.memo.instructions import MemoParams, create_memo -from .utils import assert_valid_response +from .utils import AIRDROP_AMOUNT, assert_valid_response @pytest.mark.integration def test_send_memo_in_transaction(stubbed_sender: Keypair, test_http_client: Client): """Test sending a memo instruction to localnet.""" + airdrop_resp = test_http_client.request_airdrop(stubbed_sender.public_key, AIRDROP_AMOUNT) + assert_valid_response(airdrop_resp) + test_http_client.confirm_transaction(airdrop_resp["result"]) raw_message = "test" message = bytes(raw_message, encoding="utf8") # Create memo params @@ -29,7 +32,8 @@ def test_send_memo_in_transaction(stubbed_sender: Keypair, test_http_client: Cli resp = test_http_client.send_transaction(transfer_tx, stubbed_sender) assert_valid_response(resp) txn_id = resp["result"] - test_http_client.confirm_transaction(txn_id) + # Txn needs to be finalized in order to parse the logs. + test_http_client.confirm_transaction(txn_id, commitment=Finalized) resp2 = test_http_client.get_transaction(txn_id, commitment=Finalized, encoding="jsonParsed") log_message = resp2["result"]["meta"]["logMessages"][2].split('"') assert log_message[1] == raw_message diff --git a/tests/integration/test_recent_performance_samples.py b/tests/integration/test_recent_performance_samples.py index 192128e3..cce4b1f0 100644 --- a/tests/integration/test_recent_performance_samples.py +++ b/tests/integration/test_recent_performance_samples.py @@ -1,7 +1,9 @@ """These tests live in their own file so that their sleeping doesn't slow down other tests.""" import time -from pytest import mark, fixture + +from pytest import fixture, mark + from .utils import assert_valid_response diff --git a/tests/integration/test_token_client.py b/tests/integration/test_token_client.py index eaab5a0c..237d6dc8 100644 --- a/tests/integration/test_token_client.py +++ b/tests/integration/test_token_client.py @@ -4,11 +4,12 @@ import spl.token._layouts as layouts from solana.publickey import PublicKey +from solana.rpc.commitment import Finalized from solana.utils.helpers import decode_byte_string from spl.token.client import Token from spl.token.constants import ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID -from .utils import AIRDROP_AMOUNT, assert_valid_response, OPTS +from .utils import AIRDROP_AMOUNT, OPTS, assert_valid_response @pytest.mark.integration @@ -16,9 +17,7 @@ def test_token(stubbed_sender, freeze_authority, test_http_client) -> Token: """Test create mint.""" resp = test_http_client.request_airdrop(stubbed_sender.public_key, AIRDROP_AMOUNT) - test_http_client.confirm_transaction( - resp["result"], - ) + test_http_client.confirm_transaction(resp["result"], commitment=Finalized) balance = test_http_client.get_balance(stubbed_sender.public_key) assert balance["result"]["value"] == AIRDROP_AMOUNT expected_decimals = 6 @@ -386,7 +385,11 @@ def test_thaw_account( @pytest.mark.integration def test_close_account( - stubbed_sender, stubbed_sender_token_account_pk, stubbed_receiver_token_account_pk, test_token, test_http_client + stubbed_sender, + stubbed_sender_token_account_pk, + stubbed_receiver_token_account_pk, + test_token, + test_http_client, ): # pylint: disable=redefined-outer-name """Test closing a token account.""" create_resp = test_http_client.get_account_info(stubbed_sender_token_account_pk) diff --git a/tests/integration/test_websockets.py b/tests/integration/test_websockets.py index 064fc186..b35ffb41 100644 --- a/tests/integration/test_websockets.py +++ b/tests/integration/test_websockets.py @@ -10,6 +10,7 @@ from solana.keypair import Keypair from solana.publickey import PublicKey from solana.rpc.async_api import AsyncClient +from solana.rpc.commitment import Finalized from solana.rpc.request_builder import ( AccountSubscribe, AccountUnsubscribe, @@ -147,7 +148,6 @@ async def vote_subscribed(websocket: SolanaWsClientProtocol) -> None: await websocket.vote_unsubscribe(subscription_id) -@pytest.mark.asyncio @pytest.mark.integration async def test_multiple_subscriptions( stubbed_sender: Keypair, @@ -161,13 +161,10 @@ async def test_multiple_subscriptions( assert message.result is not None if idx == len(multiple_subscriptions) - 1: break - balance = await test_http_client_async.get_balance( - stubbed_sender.public_key, - ) + balance = await test_http_client_async.get_balance(stubbed_sender.public_key, Finalized) assert balance["result"]["value"] == AIRDROP_AMOUNT -@pytest.mark.asyncio @pytest.mark.integration async def test_bad_request(websocket: SolanaWsClientProtocol): """Test sending a malformed subscription request.""" @@ -179,7 +176,6 @@ async def test_bad_request(websocket: SolanaWsClientProtocol): assert exc_info.value.subscription == bad_req -@pytest.mark.asyncio @pytest.mark.integration async def test_account_subscribe( test_http_client_async: AsyncClient, websocket: SolanaWsClientProtocol, account_subscribed: PublicKey @@ -190,7 +186,6 @@ async def test_account_subscribe( assert main_resp.result.value.lamports == AIRDROP_AMOUNT -@pytest.mark.asyncio @pytest.mark.integration async def test_logs_subscribe( test_http_client_async: AsyncClient, @@ -204,7 +199,6 @@ async def test_logs_subscribe( assert main_resp.result.value.logs[0] == "Program 11111111111111111111111111111111 invoke [1]" -@pytest.mark.asyncio @pytest.mark.integration async def test_logs_subscribe_mentions_filter( test_http_client_async: AsyncClient, @@ -218,7 +212,6 @@ async def test_logs_subscribe_mentions_filter( assert main_resp.result.value.logs[0] == "Program 11111111111111111111111111111111 invoke [1]" -@pytest.mark.asyncio @pytest.mark.integration async def test_program_subscribe( test_http_client_async: AsyncClient, @@ -235,7 +228,6 @@ async def test_program_subscribe( assert main_resp.result.value.pubkey == owned.public_key -@pytest.mark.asyncio @pytest.mark.integration async def test_signature_subscribe( websocket: SolanaWsClientProtocol, @@ -246,7 +238,6 @@ async def test_signature_subscribe( assert main_resp.result.value.err is None -@pytest.mark.asyncio @pytest.mark.integration async def test_slot_subscribe( websocket: SolanaWsClientProtocol, @@ -257,7 +248,6 @@ async def test_slot_subscribe( assert main_resp.result.root >= 0 -@pytest.mark.asyncio @pytest.mark.integration async def test_slots_updates_subscribe( websocket: SolanaWsClientProtocol, @@ -270,7 +260,6 @@ async def test_slots_updates_subscribe( break -@pytest.mark.asyncio @pytest.mark.integration async def test_root_subscribe( websocket: SolanaWsClientProtocol, @@ -281,7 +270,6 @@ async def test_root_subscribe( assert main_resp.result >= 0 -@pytest.mark.asyncio @pytest.mark.integration async def test_vote_subscribe( websocket: SolanaWsClientProtocol, diff --git a/tests/integration/utils.py b/tests/integration/utils.py index 367873e7..5f087e82 100644 --- a/tests/integration/utils.py +++ b/tests/integration/utils.py @@ -1,7 +1,7 @@ """Integration test utils.""" -from solana.rpc.types import RPCResponse, TxOpts from solana.rpc.commitment import Processed +from solana.rpc.types import RPCResponse, TxOpts AIRDROP_AMOUNT = 10_000_000_000 @@ -10,7 +10,7 @@ def assert_valid_response(resp: RPCResponse): """Assert valid RPCResponse.""" assert resp["jsonrpc"] == "2.0" assert resp["id"] - assert resp["result"] + assert resp["result"] is not None def compare_responses_without_ids(left: RPCResponse, right: RPCResponse) -> None: diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py index 73f42299..77b1ae54 100644 --- a/tests/unit/test_async_client.py +++ b/tests/unit/test_async_client.py @@ -8,7 +8,6 @@ from solana.rpc.commitment import Finalized -@pytest.mark.asyncio async def test_async_client_http_exception(unit_test_http_client_async): """Test AsyncClient raises native Solana-py exceptions."""