From 3de15cdf3b6e63d713105b51a479a9893174d464 Mon Sep 17 00:00:00 2001 From: Kevin Heavey Date: Mon, 20 Sep 2021 15:55:29 +0100 Subject: [PATCH] Add blockhash cache and allow user-supplied blockhash (#102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * allow user to pass recent blockhash themselves * add blockhash cache * add pytest-cov dev dependency * fix dupe stubbed account * linting * Bump version: 0.12.1 → 0.13.0 Co-authored-by: kevinheavey --- .bumpversion.cfg | 2 +- Pipfile | 3 + Pipfile.lock | 724 +++++++++++-------- docs/conf.py | 2 +- setup.py | 2 +- solana/blockhash.py | 47 ++ solana/rpc/api.py | 69 +- solana/rpc/async_api.py | 69 +- solana/rpc/core.py | 26 +- spl/token/async_client.py | 55 +- spl/token/client.py | 55 +- tests/conftest.py | 85 ++- tests/integration/test_async_http_client.py | 225 +++++- tests/integration/test_async_token_client.py | 190 ++--- tests/integration/test_http_client.py | 170 +++++ 15 files changed, 1227 insertions(+), 497 deletions(-) diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 774bfcfe..410ea249 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.12.1 +current_version = 0.13.0 commit = True tag = True diff --git a/Pipfile b/Pipfile index 16d9b902..6e5e8f9e 100644 --- a/Pipfile +++ b/Pipfile @@ -29,6 +29,7 @@ bump2version = "*" types-requests = "*" notebook = "*" pytest-asyncio = "*" +pytest-cov = "*" [packages] pynacl = "*" @@ -37,6 +38,8 @@ requests = "*" construct = "*" typing-extensions = "*" httpx = "*" +cachetools = "*" +types-cachetools = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 15d33958..9d496999 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e5c2b1ccfcc7c2389b56202382658ae67e89c1975701b37b40e20751ef8f263b" + "sha256": "842bc919c22935fd509bd23de139dce091d7f92e66061ac746b629d78dafaa28" }, "pipfile-spec": 6, "requires": { @@ -18,11 +18,11 @@ "default": { "anyio": { "hashes": [ - "sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0", - "sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374" + "sha256:85913b4e2fec030e8c72a8f9f98092eeb9e25847a6e00d567751b77e34f856fe", + "sha256:d7c604dd491eca70e19c78664d685d5e4337612d574419d503e76f5d7d1590bd" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.3.0" + "version": "==3.3.1" }, "base58": { "hashes": [ @@ -32,6 +32,14 @@ "index": "pypi", "version": "==2.1.0" }, + "cachetools": { + "hashes": [ + "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", + "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" + ], + "index": "pypi", + "version": "==4.2.2" + }, "certifi": { "hashes": [ "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", @@ -91,11 +99,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", + "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" ], "markers": "python_version >= '3'", - "version": "==2.0.4" + "version": "==2.0.6" }, "construct": { "hashes": [ @@ -114,19 +122,19 @@ }, "httpcore": { "hashes": [ - "sha256:b0d16f0012ec88d8cc848f5a55f8a03158405f4bca02ee49bc4ca2c1fda49f3e", - "sha256:db4c0dcb8323494d01b8c6d812d80091a31e520033e7b0120883d6f52da649ff" + "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3", + "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0" ], "markers": "python_version >= '3.6'", - "version": "==0.13.6" + "version": "==0.13.7" }, "httpx": { "hashes": [ - "sha256:979afafecb7d22a1d10340bafb403cf2cb75aff214426ff206521fc79d26408c", - "sha256:9f99c15d33642d38bce8405df088c1c4cfd940284b4290cacbfb02e64f4877c6" + "sha256:92ecd2c00c688b529eda11cedb15161eaf02dee9116712f621c70d9a40b2cdd0", + "sha256:9bd728a6c5ec0a9e243932a9983d57d3cc4a87bb4f554e1360fce407f78f9435" ], "index": "pypi", - "version": "==0.18.2" + "version": "==0.19.0" }, "idna": { "hashes": [ @@ -202,14 +210,22 @@ "markers": "python_version >= '3.5'", "version": "==1.2.0" }, + "types-cachetools": { + "hashes": [ + "sha256:8d96c270ceb5e2d90ea2651e67b96197e2997d464f1ccd3a0d0f3dff9e0463fa", + "sha256:9a44c7e1168990240520d1ff24020359e1cc5d9e2e1d63fa96bd54da2c954d07" + ], + "index": "pypi", + "version": "==4.2.0" + }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], "index": "pypi", - "version": "==3.10.0.0" + "version": "==3.10.0.2" }, "urllib3": { "hashes": [ @@ -230,61 +246,44 @@ }, "anyio": { "hashes": [ - "sha256:929a6852074397afe1d989002aa96d457e3e1e5441357c60d03e7eea0e65e1b0", - "sha256:ae57a67583e5ff8b4af47666ff5651c3732d45fd26c929253748e796af860374" + "sha256:85913b4e2fec030e8c72a8f9f98092eeb9e25847a6e00d567751b77e34f856fe", + "sha256:d7c604dd491eca70e19c78664d685d5e4337612d574419d503e76f5d7d1590bd" ], "markers": "python_full_version >= '3.6.2'", - "version": "==3.3.0" + "version": "==3.3.1" }, - "appdirs": { + "argcomplete": { "hashes": [ - "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", - "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81", + "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445" ], - "version": "==1.4.4" + "markers": "python_version < '3.8'", + "version": "==1.12.3" }, "argon2-cffi": { "hashes": [ - "sha256:05a8ac07c7026542377e38389638a8a1e9b78f1cd8439cd7493b39f08dd75fbf", - "sha256:0bf066bc049332489bb2d75f69216416329d9dc65deee127152caeb16e5ce7d5", - "sha256:18dee20e25e4be86680b178b35ccfc5d495ebd5792cd00781548d50880fee5c5", - "sha256:36320372133a003374ef4275fbfce78b7ab581440dfca9f9471be3dd9a522428", - "sha256:392c3c2ef91d12da510cfb6f9bae52512a4552573a9e27600bdb800e05905d2b", - "sha256:3aa804c0e52f208973845e8b10c70d8957c9e5a666f702793256242e9167c4e0", - "sha256:57358570592c46c420300ec94f2ff3b32cbccd10d38bdc12dc6979c4a8484fbc", - "sha256:6678bb047373f52bcff02db8afab0d2a77d83bde61cfecea7c5c62e2335cb203", - "sha256:6ea92c980586931a816d61e4faf6c192b4abce89aa767ff6581e6ddc985ed003", - "sha256:77e909cc756ef81d6abb60524d259d959bab384832f0c651ed7dcb6e5ccdbb78", - "sha256:7d455c802727710e9dfa69b74ccaab04568386ca17b0ad36350b622cd34606fe", - "sha256:8282b84ceb46b5b75c3a882b28856b8cd7e647ac71995e71b6705ec06fc232c3", - "sha256:8a84934bd818e14a17943de8099d41160da4a336bcc699bb4c394bbb9b94bd32", - "sha256:9bee3212ba4f560af397b6d7146848c32a800652301843df06b9e8f68f0f7361", - "sha256:9dfd5197852530294ecb5795c97a823839258dfd5eb9420233c7cfedec2058f2", - "sha256:b160416adc0f012fb1f12588a5e6954889510f82f698e23ed4f4fa57f12a0647", - "sha256:b94042e5dcaa5d08cf104a54bfae614be502c6f44c9c89ad1535b2ebdaacbd4c", - "sha256:ba7209b608945b889457f949cc04c8e762bed4fe3fec88ae9a6b7765ae82e496", - "sha256:cc0e028b209a5483b6846053d5fd7165f460a1f14774d79e632e75e7ae64b82b", - "sha256:d8029b2d3e4b4cea770e9e5a0104dd8fa185c1724a0f01528ae4826a6d25f97d", - "sha256:da7f0445b71db6d3a72462e04f36544b0de871289b0bc8a7cc87c0f5ec7079fa", - "sha256:e2db6e85c057c16d0bd3b4d2b04f270a7467c147381e8fd73cbbe5bc719832be" - ], - "version": "==20.1.0" + "sha256:165cadae5ac1e26644f5ade3bd9c18d89963be51d9ea8817bd671006d7909057", + "sha256:217b4f0f853ccbbb5045242946ad2e162e396064575860141b71a85eb47e475a", + "sha256:245f64a203012b144b7b8c8ea6d468cb02b37caa5afee5ba4a10c80599334f6a", + "sha256:4ad152c418f7eb640eac41ac815534e6aa61d1624530b8e7779114ecfbf327f8", + "sha256:566ffb581bbd9db5562327aee71b2eda24a1c15b23a356740abe3c011bbe0dcb", + "sha256:65213a9174320a1aee03fe826596e0620783966b49eb636955958b3074e87ff9", + "sha256:bc513db2283c385ea4da31a2cd039c33380701f376f4edd12fe56db118a3b21a", + "sha256:c7a7c8cc98ac418002090e4add5bebfff1b915ea1cb459c578cd8206fef10378", + "sha256:e4d8f0ae1524b7b0372a3e574a2561cbdddb3fdb6c28b70a72868189bda19659", + "sha256:f710b61103d1a1f692ca3ecbd1373e28aa5e545ac625ba067ff2feca1b2bb870", + "sha256:fa7e7d1fc22514a32b1761fdfa1882b6baa5c36bb3ef557bdd69e6fc9ba14a41" + ], + "markers": "python_version >= '3.5'", + "version": "==21.1.0" }, "astroid": { "hashes": [ - "sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925", - "sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f" + "sha256:dcc06f6165f415220013801642bd6c9808a02967070919c4b746c6864c205471", + "sha256:fe81f80c0b35264acb5653302ffbd935d394f1775c5e4487df745bf9c2442708" ], "markers": "python_version ~= '3.6'", - "version": "==2.6.5" - }, - "async-generator": { - "hashes": [ - "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", - "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" - ], - "markers": "python_version >= '3.5'", - "version": "==1.10" + "version": "==2.8.0" }, "attrs": { "hashes": [ @@ -324,19 +323,19 @@ }, "black": { "hashes": [ - "sha256:1c7aa6ada8ee864db745b22790a32f94b2795c253a75d6d9b5e439ff10d23116", - "sha256:c8373c6491de9362e39271630b65b964607bc5c79c83783547d76c839b3aa219" + "sha256:380f1b5da05e5a1429225676655dddb96f5ae8c75bdf91e53d798871b902a115", + "sha256:7de4cfc7eb6b710de325712d40125689101d21d25283eed7e9998722cf10eb91" ], "index": "pypi", - "version": "==21.7b0" + "version": "==21.9b0" }, "bleach": { "hashes": [ - "sha256:306483a5a9795474160ad57fce3ddd1b50551e981eed8e15a582d34cef28aafa", - "sha256:ae976d7174bba988c0b632def82fdc94235756edfb14e6558a9c5be555c9fb78" + "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da", + "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==3.3.1" + "markers": "python_version >= '3.6'", + "version": "==4.1.0" }, "bump2version": { "hashes": [ @@ -346,6 +345,14 @@ "index": "pypi", "version": "==1.0.1" }, + "cached-property": { + "hashes": [ + "sha256:9fa5755838eecbb2d234c3aa390bd80fbd3ac6b6869109bfc1b499f7bd89a130", + "sha256:df4f613cf7ad9a588cc381aaf4a512d26265ecebd5eb9e1ba12f1319eb85a6a0" + ], + "markers": "python_version < '3.8'", + "version": "==1.5.2" + }, "certifi": { "hashes": [ "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee", @@ -405,11 +412,11 @@ }, "charset-normalizer": { "hashes": [ - "sha256:0c8911edd15d19223366a194a513099a302055a962bca2cec0f54b8b63175d8b", - "sha256:f23667ebe1084be45f6ae0538e4a5a865206544097e4e8bbcacf42cd02a348f3" + "sha256:5d209c0a931f215cee683b6445e2d77677e7e75e159f78def0db09d68fafcaa6", + "sha256:5ec46d183433dcbd0ab716f2d7f29d8dee50505b3fdb40c6b985c7c4f5a3591f" ], "markers": "python_version >= '3'", - "version": "==2.0.4" + "version": "==2.0.6" }, "click": { "hashes": [ @@ -427,93 +434,119 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.4" }, + "coverage": { + "hashes": [ + "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c", + "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6", + "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45", + "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a", + "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03", + "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529", + "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a", + "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a", + "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2", + "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6", + "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759", + "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53", + "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a", + "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4", + "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff", + "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502", + "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793", + "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb", + "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905", + "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821", + "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b", + "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81", + "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0", + "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b", + "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3", + "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184", + "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701", + "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a", + "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82", + "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638", + "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5", + "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083", + "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6", + "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90", + "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465", + "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a", + "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3", + "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e", + "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066", + "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf", + "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b", + "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae", + "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669", + "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873", + "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b", + "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6", + "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb", + "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160", + "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c", + "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079", + "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d", + "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4.0'", + "version": "==5.5" + }, "cryptography": { "hashes": [ - "sha256:0f1212a66329c80d68aeeb39b8a16d54ef57071bf22ff4e521657b27372e327d", - "sha256:1e056c28420c072c5e3cb36e2b23ee55e260cb04eee08f702e0edfec3fb51959", - "sha256:240f5c21aef0b73f40bb9f78d2caff73186700bf1bc6b94285699aff98cc16c6", - "sha256:26965837447f9c82f1855e0bc8bc4fb910240b6e0d16a664bb722df3b5b06873", - "sha256:37340614f8a5d2fb9aeea67fd159bfe4f5f4ed535b1090ce8ec428b2f15a11f2", - "sha256:3d10de8116d25649631977cb37da6cbdd2d6fa0e0281d014a5b7d337255ca713", - "sha256:3d8427734c781ea5f1b41d6589c293089704d4759e34597dce91014ac125aad1", - "sha256:7ec5d3b029f5fa2b179325908b9cd93db28ab7b85bb6c1db56b10e0b54235177", - "sha256:8e56e16617872b0957d1c9742a3f94b43533447fd78321514abbe7db216aa250", - "sha256:de4e5f7f68220d92b7637fc99847475b59154b7a1b3868fb7385337af54ac9ca", - "sha256:eb8cc2afe8b05acbd84a43905832ec78e7b3873fb124ca190f574dca7389a87d", - "sha256:ee77aa129f481be46f8d92a1a7db57269a2f23052d5f2433b4621bb457081cc9" + "sha256:0a7dcbcd3f1913f664aca35d47c1331fce738d44ec34b7be8b9d332151b0b01e", + "sha256:1eb7bb0df6f6f583dd8e054689def236255161ebbcf62b226454ab9ec663746b", + "sha256:21ca464b3a4b8d8e86ba0ee5045e103a1fcfac3b39319727bc0fc58c09c6aff7", + "sha256:34dae04a0dce5730d8eb7894eab617d8a70d0c97da76b905de9efb7128ad7085", + "sha256:3520667fda779eb788ea00080124875be18f2d8f0848ec00733c0ec3bb8219fc", + "sha256:3fa3a7ccf96e826affdf1a0a9432be74dc73423125c8f96a909e3835a5ef194a", + "sha256:5b0fbfae7ff7febdb74b574055c7466da334a5371f253732d7e2e7525d570498", + "sha256:695104a9223a7239d155d7627ad912953b540929ef97ae0c34c7b8bf30857e89", + "sha256:8695456444f277af73a4877db9fc979849cd3ee74c198d04fc0776ebc3db52b9", + "sha256:94cc5ed4ceaefcbe5bf38c8fba6a21fc1d365bb8fb826ea1688e3370b2e24a1c", + "sha256:94fff993ee9bc1b2440d3b7243d488c6a3d9724cc2b09cdb297f6a886d040ef7", + "sha256:9965c46c674ba8cc572bc09a03f4c649292ee73e1b683adb1ce81e82e9a6a0fb", + "sha256:a00cf305f07b26c351d8d4e1af84ad7501eca8a342dedf24a7acb0e7b7406e14", + "sha256:a305600e7a6b7b855cd798e00278161b681ad6e9b7eca94c721d5f588ab212af", + "sha256:cd65b60cfe004790c795cc35f272e41a3df4631e2fb6b35aa7ac6ef2859d554e", + "sha256:d2a6e5ef66503da51d2110edf6c403dc6b494cc0082f85db12f54e9c5d4c3ec5", + "sha256:d9ec0e67a14f9d1d48dd87a2531009a9b251c02ea42851c060b25c782516ff06", + "sha256:f44d141b8c4ea5eb4dbc9b3ad992d45580c1d22bf5e24363f2fbf50c2d7ae8a7" ], "markers": "python_version >= '3.6'", - "version": "==3.4.7" + "version": "==3.4.8" }, "debugpy": { "hashes": [ - "sha256:00f9d14da52b87e98e26f5c3c8f1937cc496915b38f8ccb7b329336b21898678", - "sha256:129312b01ec46ab303a8c0667d559a0de0bed1a394cc128039b6f008f1c376b7", - "sha256:12cb415e7394c6738527cbc482935aa9414e9b4cc87dd040015d0e5cb8b4471a", - "sha256:1762908202b0b0b481ec44125edb625d136d16c4991d3a7c1310c85672ffe5ba", - "sha256:1bc8e835a48ef23280cbaf2b70a5a2b629b9ee79685b64d974bfb8d467f4aa67", - "sha256:2bfda2721046fb43a7074d475a12adcd55a65bfd23a1ff675427b09a01ba40cc", - "sha256:2d4c4ab934fbe1c7095d19b3d4246afe119396b49540ca5d5ad34ef01b27bd2a", - "sha256:309909b6c85f89aea3fa10fc256b52fef3c25fee4d00e1b5f5db1ace57203a2c", - "sha256:3756cd421be701d06490635372327ebd1ccb44b37d59682c994f6bd59e040a91", - "sha256:399b2c60c8e67a5d30c6e4522129e8be8d484e6064286f8ba3ce857a3927312a", - "sha256:3a6dee475102d0169732162b735878e8787500719ccb4d54b1458afe992a4c4d", - "sha256:3d92cb2e8b4f9591f6d6e17ccf8c1a55a58857949d9a5aae0ff37b64faaa3b80", - "sha256:4655824321b36b353b12d1617a29c79320412f085ecabf54524603b4c0c791e8", - "sha256:4e0d57a8c35b20b4e363db943b909aa83f12594e2f34070a1db5fa9b7213336b", - "sha256:52920ccb4acdbb2a9a42e0a4d60a7bbc4a34bf16fd23c674b280f8e9a8cacbd6", - "sha256:595170ac17567773b546d40a0ff002dc350cfcd95c9233f65e79370954fb9a01", - "sha256:67d496890d1cada5ce924cb30178684e7b82a36b80b8868beb148db54fd9e44c", - "sha256:6bb62615b3ad3d7202b7b7eb85f3d000aa17a61303af5f11eab048c91a1f30a6", - "sha256:71e67d352cabdc6a3f4dc3e39a1d2d1e76763a2102a276904e3495ede48a9832", - "sha256:732ac8bb79694cb4127c08bfc6128274f3dee9e6fd2ddde7bf026a40efeb202d", - "sha256:7376bd8f4272ab01342940bd020955f021e26954e1f0df91cfa8bf1fa4451b56", - "sha256:768f393ffaa66a3b3ed92b06e21912a5df3e01f18fb531bcbba2f94cad1725a7", - "sha256:7b332ce0d1a46f0f4200d59ee78428f18301d1fb85d07402723b94e1de96951c", - "sha256:7b4e399790a301c83ad6b153452233695b2f15450d78956a6d297859eb44d185", - "sha256:7e12e94aa2c9a0017c0a84cd475063108d06e305360b69c933bde17a6a527f80", - "sha256:84ff51b8b5c847d5421324ca419db1eec813a4dd2bbf19dbbbe132e2ab2b2fc6", - "sha256:86cd13162b752664e8ef048287a6973c8fba0a71f396b31cf36394880ec2a6bf", - "sha256:889316de0b8ff3732927cb058cfbd3371e4cd0002ecc170d34c755ad289c867c", - "sha256:89d53d57001e54a3854489e898c697aafb2d6bb81fca596da2400f3fd7fd397c", - "sha256:8a2be4e5d696ad39be6c6c37dc580993d04aad7d893fd6e449e1a055d7b5dddb", - "sha256:8e63585c372873cd88c2380c0b3c4815c724a9713f5b86d1b3a1f1ac30df079e", - "sha256:939c94d516e6ed5433cc3ba12d9d0d8108499587158ae5f76f6db18d49e21b5b", - "sha256:959d39f3d724d25b7ab79278f032e33df03c6376d51b3517abaf2f8e83594ee0", - "sha256:9a0cd73d7a76222fbc9f9180612ccb4ad7d7f7e4f26e55ef1fbd459c0f2f5322", - "sha256:9d559bd0e4c288487349e0723bc70ff06390638446ee8087d4d5711486119643", - "sha256:a19def91a0a166877c2a26b611c1ad0473ce85b1df61ae5276197375d574228b", - "sha256:a2c5a1c49239707ed5bc8e97d8f9252fb392d9e13c79c7b477593d7dde4ae24a", - "sha256:a4368c79a2c4458d5a0540381a32f8fdc02b3c9ba9dd413a49b42929297b29b3", - "sha256:a9f582203af34c6978bffaba77425662e949251998276e9dece113862e753459", - "sha256:ab37f189b1dd0d8420545c9f3d066bd1601a1ae85b26de38f5c1ccb96cf0b042", - "sha256:ac2d1cdd3279806dab2119937c0769f11dee13166650aaa84b6700b30a845d10", - "sha256:bad668e9edb21199017ab31f52a05e14506ad6566110560796d2a8f258e0b819", - "sha256:c5e771fcd12727f734caf2a10ff92966ae9857db0ccb6bebd1a4f776c54186a8", - "sha256:c96e82d863db97d3eb498cc8e55773004724bdeaa58fb0eb7ee7d5a21d240d6a", - "sha256:cd36e75c0f71a924f4b4cdb5f74b3321952cf636aadf70e0f85fd9cd2edfc1d0", - "sha256:cf6b26f26f97ef3033008db7b3df7233363407d7b6cacd4bc4f8e02ce8e11df4", - "sha256:d89ab3bd51d6a3f13b093bc3881a827d8f6c9588d9a493bddb3b47f9d078fd1d", - "sha256:dea62527a4a2770a0d12ce46564636d892bba29baaf5dba5bfe98bb55bf17a11", - "sha256:e47c42bc1a68ead3c39d9a658d3ccf311bc45dc84f3c90fa5cb7de1796243f47", - "sha256:e6711106aafc26ecb78e43c4be0a49bd0ae4a1f3e1aa502de151e38f4717b2a2", - "sha256:e7e049a4e8e362183a5a5b4ad058a1543211970819d0c11011c87c3a9dec2eaf", - "sha256:ebc241351791595796864a960892e1cd58627064feda939d0377edd0730bbff2", - "sha256:eee2224ce547d2958ffc0d63cd280a9cc6377043f32ce370cfe4ca6be4e05476", - "sha256:f20a07ac5fb0deee9be1ad1a9a124d858a8b79c66c7ec5e1767d78aa964f86c4", - "sha256:f77406f33760e6f13a7ff0ac375d9c8856844b61cd95f7502b57116858f0cfe1", - "sha256:fece69933d17e0918b73ddeb5e23bcf789edd2a6eb0d438b09c40d51e76b9c74" + "sha256:0c523fcbb6fb395403ee8508853767b74949335d5cdacc9f83d350670c2c0db2", + "sha256:135a77ac1a8f6ea49a69928f088967d36842bc492d89b45941c6b19222cffa42", + "sha256:2019ffcd08d7e643c644cd64bee0fd53c730cb8f15ff37e6a320b5afd3785bfa", + "sha256:3e4de96c70f3398abd1777f048b47564d98a40df1f72d33b47ef5b9478e07206", + "sha256:4d53fe5aecf03ba466aa7fa7474c2b2fe28b2a6c0d36688d1e29382bfe88dd5f", + "sha256:5ded60b402f83df46dee3f25ae5851809937176afdafd3fdbaab60b633b77cad", + "sha256:7c15014290150b76f0311debf7fbba2e934680572ea60750b0f048143e873b3e", + "sha256:7e7210a3721fc54b52d8dc2f325e7c937ffcbba02b808e2e3215dcbf0c0b8349", + "sha256:847926f78c1e33f7318a743837adb6a9b360a825b558fd21f9240ba518fe1bb1", + "sha256:88b17d7c2130968f75bdc706a33f46a8a6bb90f09512ea3bd984659d446ee4f4", + "sha256:8d488356cc66172f1ea29635fd148ad131f13fad0e368ae03cc5c0a402372756", + "sha256:ab3f33499c597a2ce454b81088e7f9d56127686e003c4f7a1c97ad4b38a55404", + "sha256:c0fd1a66e104752f86ca2faa6a0194dae61442a768f85369fc3d11bacff8120f", + "sha256:c3d7db37b7eb234e49f50ba22b3b1637e8daadd68985d9cd35a6152aa10faa75", + "sha256:c9665e58b80d839ae1b0815341c63d00cae557c018f198c0b6b7bc5de9eca144", + "sha256:dbda8f877c3dec1559c01c63a1de63969e51a4907dc308f4824238bb776026fe", + "sha256:f3dcc294f3b4d79fdd7ffe1350d5d1e3cc29acaec67dd1c43143a43305bbbc91", + "sha256:f907941ad7a460646773eb3baae4c88836e9256b390dfbfae8d92a3d3b849a7d" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.4.1" + "version": "==1.4.3" }, "decorator": { "hashes": [ - "sha256:6e5c199c16f7a9f0e3a61a4a54b3d27e7dad0dbdde92b944426cb20914376323", - "sha256:72ecfba4320a893c53f9706bebb2d55c270c1e51a28789361aa93e4a21319ed5" + "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374", + "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7" ], "markers": "python_version >= '3.5'", - "version": "==5.0.9" + "version": "==5.1.0" }, "defusedxml": { "hashes": [ @@ -535,11 +568,11 @@ "ssh" ], "hashes": [ - "sha256:3e8bc47534e0ca9331d72c32f2881bb13b93ded0bcdeab3c833fb7cf61c0a9a5", - "sha256:fc961d622160e8021c10d1bcabc388c57d55fb1f917175afbe24af442e6879bd" + "sha256:21ec4998e90dff7a7aaaa098ca8d839c7de412b89e6f6c30908372d58fecf663", + "sha256:9b17f0723d83c1f3418d2aa17bf90b24dbe97deda06208dd4262fa30a6ee87eb" ], "markers": "python_version >= '3.6'", - "version": "==5.0.0" + "version": "==5.0.2" }, "docker-compose": { "hashes": [ @@ -603,11 +636,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:0645585859e9a6689c523927a5032f2ba5919f1f7d0e84bd4533312320de1ff9", - "sha256:51c6635429c77cf1ae634c997ff9e53ca3438b495f10a55ba28594dd69764a8b" + "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15", + "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1" ], - "markers": "python_version >= '3.6'", - "version": "==4.6.3" + "markers": "python_version == '3.7' and python_version < '3.8'", + "version": "==4.8.1" }, "iniconfig": { "hashes": [ @@ -618,19 +651,19 @@ }, "ipykernel": { "hashes": [ - "sha256:0df34a78c7e1422800d6078cde65ccdcdb859597046c338c759db4dbc535c58f", - "sha256:9f9f41a14caf2fde2b7802446adf83885afcbf50585a46d6c687292599a3c3af" + "sha256:a3f6c2dda2ecf63b37446808a70ed825fea04790779ca524889c596deae0def8", + "sha256:df3355e5eec23126bc89767a676c5f0abfc7f4c3497d118c592b83b316e8c0cd" ], "markers": "python_version >= '3.7'", - "version": "==6.0.3" + "version": "==6.4.1" }, "ipython": { "hashes": [ - "sha256:0cff04bb042800129348701f7bd68a430a844e8fb193979c08f6c99f28bb735e", - "sha256:892743b65c21ed72b806a3a602cca408520b3200b89d1924f4b3d2cdb3692362" + "sha256:58b55ebfdfa260dad10d509702dc2857cb25ad82609506b070cf2d7b7df5af13", + "sha256:75b5e060a3417cf64f138e0bb78e58512742c57dc29db5a5058a2b1f0c10df02" ], "markers": "python_version >= '3.7'", - "version": "==7.26.0" + "version": "==7.27.0" }, "ipython-genutils": { "hashes": [ @@ -687,35 +720,35 @@ }, "jupyter-client": { "hashes": [ - "sha256:c4bca1d0846186ca8be97f4d2fa6d2bae889cce4892a167ffa1ba6bd1f73e782", - "sha256:e053a2c44b6fa597feebe2b3ecb5eea3e03d1d91cc94351a52931ee1426aecfc" + "sha256:b07ceecb8f845f908bbd0f78bb17c0abac7b393de9d929bd92190e36c24c201e", + "sha256:bb58e3218d74e072673948bd1e2a6bb3b65f32447b3e8c143eeca16b946ee230" ], - "markers": "python_version >= '3.5'", - "version": "==6.1.12" + "markers": "python_full_version >= '3.6.1'", + "version": "==7.0.3" }, "jupyter-core": { "hashes": [ - "sha256:79025cb3225efcd36847d0840f3fc672c0abd7afd0de83ba8a1d3837619122b4", - "sha256:8c6c0cac5c1b563622ad49321d5ec47017bd18b94facb381c6973a0486395f8e" + "sha256:8dd262ec8afae95bd512518eb003bc546b76adbf34bf99410e9accdf4be9aa3a", + "sha256:ef210dcb4fca04de07f2ead4adf408776aca94d17151d6f750ad6ded0b91ea16" ], "markers": "python_version >= '3.6'", - "version": "==4.7.1" + "version": "==4.8.1" }, "jupyter-server": { "hashes": [ - "sha256:491c920013144a2d6f5286ab4038df6a081b32352c9c8b928ec8af17eb2a5e10", - "sha256:d3a3b68ebc6d7bfee1097f1712cf7709ee39c92379da2cc08724515bb85e72bf" + "sha256:827c134da7a9e09136c2dc2fd16743350970105247f085abfc6ce0432d0c979e", + "sha256:8ab4f484a4a2698f757cff0769d27b5d991e0232a666d54f4d6ada4e6a61330b" ], "markers": "python_version >= '3.6'", - "version": "==1.10.2" + "version": "==1.11.0" }, "jupyterlab": { "hashes": [ - "sha256:a181184b1000a550c38da35471dcf91ce11e96750de56430be3fc93ca01dde1e", - "sha256:f6b04b5cfbe1fab79dbcecd58c941cbc73b0da5e1ccc5d4333a36860d37789b0" + "sha256:69745b3e1333b98054fe247efdc7fff16a15334b8a96a8070dffd350b9733017", + "sha256:d16d244b09ff6c101672637101313263bfd76d4c057bfe79a401fea54e1be0e9" ], "index": "pypi", - "version": "==3.1.1" + "version": "==3.1.12" }, "jupyterlab-pygments": { "hashes": [ @@ -726,19 +759,19 @@ }, "jupyterlab-server": { "hashes": [ - "sha256:58d4b660fce8da4e90f0433ac54f462436fe5fbe731e3a281e15adcdecddb0eb", - "sha256:73279d1ffdcd3426f716bf5538cf1fdd2eb8a340ac25c5688f3c192c5bd3afc9" + "sha256:39fd519e9b3275873bd15de891363c28f2649814f7bbc11c57469c60e8408e97", + "sha256:bfbfbf9886c7fae60d238d458b0eff409528aa286731f01ad1308793e8b5b209" ], "markers": "python_version >= '3.6'", - "version": "==2.6.1" + "version": "==2.8.1" }, "keyring": { "hashes": [ - "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8", - "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48" + "sha256:6334aee6073db2fb1f30892697b1730105b5e9a77ce7e61fca6b435225493efe", + "sha256:bd2145a237ed70c8ce72978b497619ddfcae640b6dcf494402d5143e37755c6e" ], "markers": "python_version >= '3.6'", - "version": "==23.0.1" + "version": "==23.2.1" }, "lazy-object-proxy": { "hashes": [ @@ -775,30 +808,50 @@ "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b", "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567", "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff", + "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724", "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74", + "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646", "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35", + "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6", + "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6", + "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad", "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26", + "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38", + "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac", "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7", + "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6", "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75", "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f", "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135", "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8", + "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a", "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a", + "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9", + "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864", "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914", "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18", "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8", "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2", "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d", + "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b", "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b", "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f", "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb", "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833", + "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28", "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415", "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902", + "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d", "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9", "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d", + "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145", "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066", + "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c", + "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1", "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f", + "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53", + "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134", + "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85", "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5", "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94", "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509", @@ -810,11 +863,11 @@ }, "matplotlib-inline": { "hashes": [ - "sha256:5cf1176f554abb4fa98cb362aa2b55c500147e4bdbb07e3fda359143e1da0811", - "sha256:f41d5ff73c9f5385775d5c0bc13b424535c8402fe70ea8210f93e11f3683993e" + "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee", + "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c" ], "markers": "python_version >= '3.5'", - "version": "==0.1.2" + "version": "==0.1.3" }, "mccabe": { "hashes": [ @@ -868,19 +921,19 @@ }, "nbclassic": { "hashes": [ - "sha256:a7437c90a0bffcce172a4540cc53e140ea5987280c87c31a0cfa6e5d315eb907", - "sha256:f920f8d09849bea7950e1017ff3bd101763a8d68f565a51ce053572e65aa7947" + "sha256:57936a39410a18261442ca3b298421f859c9012272b87bf55e17b5507f052f4d", + "sha256:863462bf6a6e0e5e502dcc479ce2ea1edf60437c969f1850d0c0823dba0c39b7" ], "markers": "python_version >= '3.6'", - "version": "==0.3.1" + "version": "==0.3.2" }, "nbclient": { "hashes": [ - "sha256:db17271330c68c8c88d46d72349e24c147bb6f34ec82d8481a8f025c4d26589c", - "sha256:e79437364a2376892b3f46bedbf9b444e5396cfb1bc366a472c37b48e9551500" + "sha256:6c8ad36a28edad4562580847f9f1636fe5316a51a323ed85a24a4ad37d4aefce", + "sha256:95a300c6fbe73721736cf13972a46d8d666f78794b832866ed7197a504269e11" ], "markers": "python_full_version >= '3.6.1'", - "version": "==0.5.3" + "version": "==0.5.4" }, "nbconvert": { "hashes": [ @@ -908,11 +961,11 @@ }, "notebook": { "hashes": [ - "sha256:9c4625e2a2aa49d6eae4ce20cbc3d8976db19267e32d2a304880e0c10bf8aef9", - "sha256:f7f0a71a999c7967d9418272ae4c3378a220bd28330fbfb49860e46cf8a5838a" + "sha256:26b0095c568e307a310fd78818ad8ebade4f00462dada4c0e34cbad632b9085d", + "sha256:33488bdcc5cbef23c3cfa12cd51b0b5459a211945b5053d17405980611818149" ], "index": "pypi", - "version": "==6.4.0" + "version": "==6.4.4" }, "packaging": { "hashes": [ @@ -924,9 +977,11 @@ }, "pandocfilters": { "hashes": [ - "sha256:bc63fbb50534b4b1f8ebe1860889289e8af94a23bff7445259592df25a3906eb" + "sha256:0b679503337d233b4339a817bfc8c50064e2eff681314376a47cb582305a7a38", + "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f" ], - "version": "==1.4.3" + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.5.0" }, "paramiko": { "hashes": [ @@ -972,13 +1027,21 @@ ], "version": "==1.7.1" }, + "platformdirs": { + "hashes": [ + "sha256:15b056538719b1c94bdaccb29e5f81879c7f7f0f4a153f46086d155dffcd4f0f", + "sha256:8003ac87717ae2c7ee1ea5a84a1a61e87f3fbd16eb5aadba194ea30a9019f648" + ], + "markers": "python_version >= '3.6'", + "version": "==2.3.0" + }, "pluggy": { "hashes": [ - "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0", - "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d" + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==0.13.1" + "markers": "python_version >= '3.6'", + "version": "==1.0.0" }, "prometheus-client": { "hashes": [ @@ -990,11 +1053,11 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:08360ee3a3148bdb5163621709ee322ec34fc4375099afa4bbf751e9b7b7fa4f", - "sha256:7089d8d2938043508aa9420ec18ce0922885304cddae87fb96eebca942299f88" + "sha256:6076e46efae19b1e0ca1ec003ed37a933dc94b4d20f486235d436e64771dcd5c", + "sha256:eb71d5a6b72ce6db177af4a7d4d7085b99756bf656d98ffcc4fecd36850eea6c" ], - "markers": "python_full_version >= '3.6.1'", - "version": "==3.0.19" + "markers": "python_full_version >= '3.6.2'", + "version": "==3.0.20" }, "ptyprocess": { "hashes": [ @@ -1046,19 +1109,19 @@ }, "pygments": { "hashes": [ - "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f", - "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e" + "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380", + "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6" ], "markers": "python_version >= '3.5'", - "version": "==2.9.0" + "version": "==2.10.0" }, "pylint": { "hashes": [ - "sha256:2e1a0eb2e8ab41d6b5dbada87f066492bb1557b12b76c47c2ee8aa8a11186594", - "sha256:8b838c8983ee1904b2de66cce9d0b96649a91901350e956d78f289c3bc87b48e" + "sha256:0f358e221c45cbd4dad2a1e4b883e75d28acdcccd29d40c76eb72b307269b126", + "sha256:2c9843fff1a88ca0ad98a256806c82c5a8f86086e7ccbdb93297d86c3f90c436" ], "index": "pypi", - "version": "==2.9.6" + "version": "==2.11.1" }, "pynacl": { "hashes": [ @@ -1121,11 +1184,11 @@ }, "pytest": { "hashes": [ - "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b", - "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890" + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" ], "index": "pypi", - "version": "==6.2.4" + "version": "==6.2.5" }, "pytest-asyncio": { "hashes": [ @@ -1135,6 +1198,14 @@ "index": "pypi", "version": "==0.15.1" }, + "pytest-cov": { + "hashes": [ + "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a", + "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7" + ], + "index": "pypi", + "version": "==2.12.1" + }, "pytest-docker": { "hashes": [ "sha256:292d23c5a1745aaa71b23edc24f410e9c4b1f52cb1ead859c923bd4438bac3b8", @@ -1203,41 +1274,46 @@ }, "pyzmq": { "hashes": [ - "sha256:089b974ec04d663b8685ac90e86bfe0e4da9d911ff3cf52cb765ff22408b102d", - "sha256:0ea7f4237991b0f745a4432c63e888450840bf8cb6c48b93fb7d62864f455529", - "sha256:0f0f27eaab9ba7b92d73d71c51d1a04464a1da6097a252d007922103253d2313", - "sha256:12ffcf33db6ba7c0e5aaf901e65517f5e2b719367b80bcbfad692f546a297c7a", - "sha256:1389b615917d4196962a9b469e947ba862a8ec6f5094a47da5e7a8d404bc07a4", - "sha256:18dd2ca4540c476558099891c129e6f94109971d110b549db2a9775c817cedbd", - "sha256:24fb5bb641f0b2aa25fc3832f4b6fc62430f14a7d328229fe994b2bcdc07c93a", - "sha256:285514956c08c7830da9d94e01f5414661a987831bd9f95e4d89cc8aaae8da10", - "sha256:41049cff5265e9cd75606aa2c90a76b9c80b98d8fe70ee08cf4af3cedb113358", - "sha256:461ed80d741692d9457ab820b1cc057ba9c37c394e67b647b639f623c8b321f6", - "sha256:4b8fb1b3174b56fd020e4b10232b1764e52cf7f3babcfb460c5253bdc48adad0", - "sha256:4c4fe69c7dc0d13d4ae180ad650bb900854367f3349d3c16f0569f6c6447f698", - "sha256:4e9b9a2f6944acdaf57316436c1acdcb30b8df76726bcf570ad9342bc5001654", - "sha256:6355f81947e1fe6e7bb9e123aeb3067264391d3ebe8402709f824ef8673fa6f3", - "sha256:68be16107f41563b9f67d93dff1c9f5587e0f76aa8fd91dc04c83d813bcdab1f", - "sha256:68e2c4505992ab5b89f976f89a9135742b18d60068f761bef994a6805f1cae0c", - "sha256:7040d6dd85ea65703904d023d7f57fab793d7ffee9ba9e14f3b897f34ff2415d", - "sha256:734ea6565c71fc2d03d5b8c7d0d7519c96bb5567e0396da1b563c24a4ac66f0c", - "sha256:9ee48413a2d3cd867fd836737b4c89c24cea1150a37f4856d82d20293fa7519f", - "sha256:a1c77796f395804d6002ff56a6a8168c1f98579896897ad7e35665a9b4a9eec5", - "sha256:b2f707b52e09098a7770503e39294ca6e22ae5138ffa1dd36248b6436d23d78e", - "sha256:bf80b2cec42d96117248b99d3c86e263a00469c840a778e6cb52d916f4fdf82c", - "sha256:c4674004ed64685a38bee222cd75afa769424ec603f9329f0dd4777138337f48", - "sha256:c6a81c9e6754465d09a87e3acd74d9bb1f0039b2d785c6899622f0afdb41d760", - "sha256:c6d0c32532a0519997e1ded767e184ebb8543bdb351f8eff8570bd461e874efc", - "sha256:c8fff75af4c7af92dce9f81fa2a83ed009c3e1f33ee8b5222db2ef80b94e242e", - "sha256:cb9f9fe1305ef69b65794655fd89b2209b11bff3e837de981820a8aa051ef914", - "sha256:d3ecfee2ee8d91ab2e08d2d8e89302c729b244e302bbc39c5b5dde42306ff003", - "sha256:d5e5be93e1714a59a535bbbc086b9e4fd2448c7547c5288548f6fd86353cad9e", - "sha256:de5806be66c9108e4dcdaced084e8ceae14100aa559e2d57b4f0cceb98c462de", - "sha256:f49755684a963731479ff3035d45a8185545b4c9f662d368bd349c419839886d", - "sha256:fc712a90401bcbf3fa25747f189d6dcfccbecc32712701cad25c6355589dac57" + "sha256:0ca6cd58f62a2751728016d40082008d3b3412a7f28ddfb4a2f0d3c130f69e74", + "sha256:1621e7a2af72cced1f6ec8ca8ca91d0f76ac236ab2e8828ac8fe909512d566cb", + "sha256:18cd854b423fce44951c3a4d3e686bac8f1243d954f579e120a1714096637cc0", + "sha256:2841997a0d85b998cbafecb4183caf51fd19c4357075dfd33eb7efea57e4c149", + "sha256:2b97502c16a5ec611cd52410bdfaab264997c627a46b0f98d3f666227fd1ea2d", + "sha256:3a4c9886d61d386b2b493377d980f502186cd71d501fffdba52bd2a0880cef4f", + "sha256:3c1895c95be92600233e476fe283f042e71cf8f0b938aabf21b7aafa62a8dac9", + "sha256:42abddebe2c6a35180ca549fadc7228d23c1e1f76167c5ebc8a936b5804ea2df", + "sha256:480b9931bfb08bf8b094edd4836271d4d6b44150da051547d8c7113bf947a8b0", + "sha256:67db33bea0a29d03e6eeec55a8190e033318cee3cbc732ba8fd939617cbf762d", + "sha256:6b217b8f9dfb6628f74b94bdaf9f7408708cb02167d644edca33f38746ca12dd", + "sha256:7661fc1d5cb73481cf710a1418a4e1e301ed7d5d924f91c67ba84b2a1b89defd", + "sha256:76c532fd68b93998aab92356be280deec5de8f8fe59cd28763d2cc8a58747b7f", + "sha256:79244b9e97948eaf38695f4b8e6fc63b14b78cc37f403c6642ba555517ac1268", + "sha256:7c58f598d9fcc52772b89a92d72bf8829c12d09746a6d2c724c5b30076c1f11d", + "sha256:7dc09198e4073e6015d9a8ea093fc348d4e59de49382476940c3dd9ae156fba8", + "sha256:80e043a89c6cadefd3a0712f8a1322038e819ebe9dbac7eca3bce1721bcb63bf", + "sha256:851977788b9caa8ed011f5f643d3ee8653af02c5fc723fa350db5125abf2be7b", + "sha256:8eddc033e716f8c91c6a2112f0a8ebc5e00532b4a6ae1eb0ccc48e027f9c671c", + "sha256:954e73c9cd4d6ae319f1c936ad159072b6d356a92dcbbabfd6e6204b9a79d356", + "sha256:ab888624ed68930442a3f3b0b921ad7439c51ba122dbc8c386e6487a658e4a4e", + "sha256:acebba1a23fb9d72b42471c3771b6f2f18dcd46df77482612054bd45c07dfa36", + "sha256:b4ebed0977f92320f6686c96e9e8dd29eed199eb8d066936bac991afc37cbb70", + "sha256:be4e0f229cf3a71f9ecd633566bd6f80d9fa6afaaff5489492be63fe459ef98c", + "sha256:c0f84360dcca3481e8674393bdf931f9f10470988f87311b19d23cda869bb6b7", + "sha256:c1e41b32d6f7f9c26bc731a8b529ff592f31fc8b6ef2be9fa74abd05c8a342d7", + "sha256:cf98fd7a6c8aaa08dbc699ffae33fd71175696d78028281bc7b832b26f00ca57", + "sha256:d072f7dfbdb184f0786d63bda26e8a0882041b1e393fbe98940395f7fab4c5e2", + "sha256:d3dcb5548ead4f1123851a5ced467791f6986d68c656bc63bfff1bf9e36671e2", + "sha256:d6157793719de168b199194f6b6173f0ccd3bf3499e6870fac17086072e39115", + "sha256:d728b08448e5ac3e4d886b165385a262883c34b84a7fe1166277fe675e1c197a", + "sha256:de8df0684398bd74ad160afdc2a118ca28384ac6f5e234eb0508858d8d2d9364", + "sha256:e6a02cf7271ee94674a44f4e62aa061d2d049001c844657740e156596298b70b", + "sha256:ea12133df25e3a6918718fbb9a510c6ee5d3fdd5a346320421aac3882f4feeea", + "sha256:f43b4a2e6218371dd4f41e547bd919ceeb6ebf4abf31a7a0669cd11cd91ea973", + "sha256:f762442bab706fd874064ca218b33a1d8e40d4938e96c24dafd9b12e28017f45", + "sha256:f89468059ebc519a7acde1ee50b779019535db8dcf9b8c162ef669257fef7a93" ], "markers": "python_version >= '3.6'", - "version": "==22.1.0" + "version": "==22.3.0" }, "readme-renderer": { "hashes": [ @@ -1248,49 +1324,49 @@ }, "regex": { "hashes": [ - "sha256:0eb2c6e0fcec5e0f1d3bcc1133556563222a2ffd2211945d7b1480c1b1a42a6f", - "sha256:15dddb19823f5147e7517bb12635b3c82e6f2a3a6b696cc3e321522e8b9308ad", - "sha256:173bc44ff95bc1e96398c38f3629d86fa72e539c79900283afa895694229fe6a", - "sha256:1c78780bf46d620ff4fff40728f98b8afd8b8e35c3efd638c7df67be2d5cddbf", - "sha256:2366fe0479ca0e9afa534174faa2beae87847d208d457d200183f28c74eaea59", - "sha256:2bceeb491b38225b1fee4517107b8491ba54fba77cf22a12e996d96a3c55613d", - "sha256:2ddeabc7652024803666ea09f32dd1ed40a0579b6fbb2a213eba590683025895", - "sha256:2fe5e71e11a54e3355fa272137d521a40aace5d937d08b494bed4529964c19c4", - "sha256:319eb2a8d0888fa6f1d9177705f341bc9455a2c8aca130016e52c7fe8d6c37a3", - "sha256:3f5716923d3d0bfb27048242a6e0f14eecdb2e2a7fac47eda1d055288595f222", - "sha256:422dec1e7cbb2efbbe50e3f1de36b82906def93ed48da12d1714cabcd993d7f0", - "sha256:4c9c3155fe74269f61e27617529b7f09552fbb12e44b1189cebbdb24294e6e1c", - "sha256:4f64fc59fd5b10557f6cd0937e1597af022ad9b27d454e182485f1db3008f417", - "sha256:564a4c8a29435d1f2256ba247a0315325ea63335508ad8ed938a4f14c4116a5d", - "sha256:59506c6e8bd9306cd8a41511e32d16d5d1194110b8cfe5a11d102d8b63cf945d", - "sha256:598c0a79b4b851b922f504f9f39a863d83ebdfff787261a5ed061c21e67dd761", - "sha256:59c00bb8dd8775473cbfb967925ad2c3ecc8886b3b2d0c90a8e2707e06c743f0", - "sha256:6110bab7eab6566492618540c70edd4d2a18f40ca1d51d704f1d81c52d245026", - "sha256:6afe6a627888c9a6cfbb603d1d017ce204cebd589d66e0703309b8048c3b0854", - "sha256:791aa1b300e5b6e5d597c37c346fb4d66422178566bbb426dd87eaae475053fb", - "sha256:8394e266005f2d8c6f0bc6780001f7afa3ef81a7a2111fa35058ded6fce79e4d", - "sha256:875c355360d0f8d3d827e462b29ea7682bf52327d500a4f837e934e9e4656068", - "sha256:89e5528803566af4df368df2d6f503c84fbfb8249e6631c7b025fe23e6bd0cde", - "sha256:99d8ab206a5270c1002bfcf25c51bf329ca951e5a169f3b43214fdda1f0b5f0d", - "sha256:9a854b916806c7e3b40e6616ac9e85d3cdb7649d9e6590653deb5b341a736cec", - "sha256:b85ac458354165405c8a84725de7bbd07b00d9f72c31a60ffbf96bb38d3e25fa", - "sha256:bc84fb254a875a9f66616ed4538542fb7965db6356f3df571d783f7c8d256edd", - "sha256:c92831dac113a6e0ab28bc98f33781383fe294df1a2c3dfd1e850114da35fd5b", - "sha256:cbe23b323988a04c3e5b0c387fe3f8f363bf06c0680daf775875d979e376bd26", - "sha256:ccb3d2190476d00414aab36cca453e4596e8f70a206e2aa8db3d495a109153d2", - "sha256:d8bbce0c96462dbceaa7ac4a7dfbbee92745b801b24bce10a98d2f2b1ea9432f", - "sha256:db2b7df831c3187a37f3bb80ec095f249fa276dbe09abd3d35297fc250385694", - "sha256:e586f448df2bbc37dfadccdb7ccd125c62b4348cb90c10840d695592aa1b29e0", - "sha256:e5983c19d0beb6af88cb4d47afb92d96751fb3fa1784d8785b1cdf14c6519407", - "sha256:e6a1e5ca97d411a461041d057348e578dc344ecd2add3555aedba3b408c9f874", - "sha256:eaf58b9e30e0e546cdc3ac06cf9165a1ca5b3de8221e9df679416ca667972035", - "sha256:ed693137a9187052fc46eedfafdcb74e09917166362af4cc4fddc3b31560e93d", - "sha256:edd1a68f79b89b0c57339bce297ad5d5ffcc6ae7e1afdb10f1947706ed066c9c", - "sha256:f080248b3e029d052bf74a897b9d74cfb7643537fbde97fe8225a6467fb559b5", - "sha256:f9392a4555f3e4cb45310a65b403d86b589adc773898c25a39184b1ba4db8985", - "sha256:f98dc35ab9a749276f1a4a38ab3e0e2ba1662ce710f6530f5b0a6656f1c32b58" - ], - "version": "==2021.7.6" + "sha256:04f6b9749e335bb0d2f68c707f23bb1773c3fb6ecd10edf0f04df12a8920d468", + "sha256:08d74bfaa4c7731b8dac0a992c63673a2782758f7cfad34cf9c1b9184f911354", + "sha256:0fc1f8f06977c2d4f5e3d3f0d4a08089be783973fc6b6e278bde01f0544ff308", + "sha256:121f4b3185feaade3f85f70294aef3f777199e9b5c0c0245c774ae884b110a2d", + "sha256:1413b5022ed6ac0d504ba425ef02549a57d0f4276de58e3ab7e82437892704fc", + "sha256:1743345e30917e8c574f273f51679c294effba6ad372db1967852f12c76759d8", + "sha256:28fc475f560d8f67cc8767b94db4c9440210f6958495aeae70fac8faec631797", + "sha256:31a99a4796bf5aefc8351e98507b09e1b09115574f7c9dbb9cf2111f7220d2e2", + "sha256:328a1fad67445550b982caa2a2a850da5989fd6595e858f02d04636e7f8b0b13", + "sha256:473858730ef6d6ff7f7d5f19452184cd0caa062a20047f6d6f3e135a4648865d", + "sha256:4cde065ab33bcaab774d84096fae266d9301d1a2f5519d7bd58fc55274afbf7a", + "sha256:5f6a808044faae658f546dd5f525e921de9fa409de7a5570865467f03a626fc0", + "sha256:610b690b406653c84b7cb6091facb3033500ee81089867ee7d59e675f9ca2b73", + "sha256:66256b6391c057305e5ae9209941ef63c33a476b73772ca967d4a2df70520ec1", + "sha256:6eebf512aa90751d5ef6a7c2ac9d60113f32e86e5687326a50d7686e309f66ed", + "sha256:79aef6b5cd41feff359acaf98e040844613ff5298d0d19c455b3d9ae0bc8c35a", + "sha256:808ee5834e06f57978da3e003ad9d6292de69d2bf6263662a1a8ae30788e080b", + "sha256:8e44769068d33e0ea6ccdf4b84d80c5afffe5207aa4d1881a629cf0ef3ec398f", + "sha256:999ad08220467b6ad4bd3dd34e65329dd5d0df9b31e47106105e407954965256", + "sha256:9b006628fe43aa69259ec04ca258d88ed19b64791693df59c422b607b6ece8bb", + "sha256:9d05ad5367c90814099000442b2125535e9d77581855b9bee8780f1b41f2b1a2", + "sha256:a577a21de2ef8059b58f79ff76a4da81c45a75fe0bfb09bc8b7bb4293fa18983", + "sha256:a617593aeacc7a691cc4af4a4410031654f2909053bd8c8e7db837f179a630eb", + "sha256:abb48494d88e8a82601af905143e0de838c776c1241d92021e9256d5515b3645", + "sha256:ac88856a8cbccfc14f1b2d0b829af354cc1743cb375e7f04251ae73b2af6adf8", + "sha256:b4c220a1fe0d2c622493b0a1fd48f8f991998fb447d3cd368033a4b86cf1127a", + "sha256:b844fb09bd9936ed158ff9df0ab601e2045b316b17aa8b931857365ea8586906", + "sha256:bdc178caebd0f338d57ae445ef8e9b737ddf8fbc3ea187603f65aec5b041248f", + "sha256:c206587c83e795d417ed3adc8453a791f6d36b67c81416676cad053b4104152c", + "sha256:c61dcc1cf9fd165127a2853e2c31eb4fb961a4f26b394ac9fe5669c7a6592892", + "sha256:c7cb4c512d2d3b0870e00fbbac2f291d4b4bf2634d59a31176a87afe2777c6f0", + "sha256:d4a332404baa6665b54e5d283b4262f41f2103c255897084ec8f5487ce7b9e8e", + "sha256:d5111d4c843d80202e62b4fdbb4920db1dcee4f9366d6b03294f45ed7b18b42e", + "sha256:e1e8406b895aba6caa63d9fd1b6b1700d7e4825f78ccb1e5260551d168db38ed", + "sha256:e8690ed94481f219a7a967c118abaf71ccc440f69acd583cab721b90eeedb77c", + "sha256:ed283ab3a01d8b53de3a05bfdf4473ae24e43caee7dcb5584e86f3f3e5ab4374", + "sha256:ed4b50355b066796dacdd1cf538f2ce57275d001838f9b132fab80b75e8c84dd", + "sha256:ee329d0387b5b41a5dddbb6243a21cb7896587a651bebb957e2d2bb8b63c0791", + "sha256:f3bf1bc02bc421047bfec3343729c4bbbea42605bcfd6d6bfe2c07ade8b12d2a", + "sha256:f585cbbeecb35f35609edccb95efd95a3e35824cd7752b586503f7e6087303f1", + "sha256:f60667673ff9c249709160529ab39667d1ae9fd38634e006bec95611f632e759" + ], + "version": "==2021.8.28" }, "requests": { "hashes": [ @@ -1334,10 +1410,10 @@ }, "send2trash": { "hashes": [ - "sha256:17730aa0a33ab82ed6ca76be3bb25f0433d0014f1ccf63c979bab13a5b9db2b2", - "sha256:c20fee8c09378231b3907df9c215ec9766a84ee20053d99fbad854fe8bd42159" + "sha256:d2c24762fd3759860a0aff155e45871447ea58d2be6bdd39b5c8f966a0c99c2d", + "sha256:f20eaadfdb517eaca5ce077640cb261c7d2698385a6a0f072a4a5447fd49fa08" ], - "version": "==1.7.1" + "version": "==1.8.0" }, "six": { "hashes": [ @@ -1364,11 +1440,11 @@ }, "sphinx": { "hashes": [ - "sha256:3092d929cd807926d846018f2ace47ba2f3b671b309c7a89cd3306e80c826b13", - "sha256:46d52c6cee13fec44744b8c01ed692c18a640f6910a725cbb938bc36e8d64544" + "sha256:94078db9184491e15bce0a56d9186e0aec95f16ac20b12d00e06d4e36f1058a6", + "sha256:98a535c62a4fcfcc362528592f69b26f7caec587d32cd55688db580be0287ae0" ], "index": "pypi", - "version": "==4.1.2" + "version": "==4.2.0" }, "sphinxcontrib-applehelp": { "hashes": [ @@ -1420,11 +1496,11 @@ }, "terminado": { "hashes": [ - "sha256:89d5dac2f4e2b39758a0ff9a3b643707c95a020a6df36e70583b88297cd59cbe", - "sha256:c89ace5bffd0e7268bdcf22526830eb787fd146ff9d78691a0528386f92b9ae3" + "sha256:09fdde344324a1c9c6e610ee4ca165c4bb7f5bbf982fceeeb38998a988ef8452", + "sha256:b20fd93cc57c1678c799799d117874367cc07a3d2d55be95205b1a88fa08393f" ], "markers": "python_version >= '3.6'", - "version": "==0.10.1" + "version": "==0.12.1" }, "testpath": { "hashes": [ @@ -1451,11 +1527,11 @@ }, "tomli": { "hashes": [ - "sha256:056f0376bf5a6b182c513f9582c1e5b0487265eb6c48842b69aa9ca1cd5f640a", - "sha256:d60e681734099207a6add7a10326bc2ddd1fdc36c1b0f547d00ef73ac63739c2" + "sha256:8dd0e9524d6f386271a36b41dbf6c57d8e32fd96fd22b6584679dc569d20899f", + "sha256:a5b75cb6f3968abb47af1b40c1819dc519ea82bcc065776a866e8d74c5ca9442" ], "markers": "python_version >= '3.6'", - "version": "==1.2.0" + "version": "==1.2.1" }, "tornado": { "hashes": [ @@ -1506,19 +1582,19 @@ }, "tqdm": { "hashes": [ - "sha256:3642d483b558eec80d3c831e23953582c34d7e4540db86d9e5ed9dad238dabc6", - "sha256:706dea48ee05ba16e936ee91cb3791cd2ea6da348a0e50b46863ff4363ff4340" + "sha256:80aead664e6c1672c4ae20dc50e1cdc5e20eeff9b14aa23ecd426375b28be588", + "sha256:a4d6d112e507ef98513ac119ead1159d286deab17dffedd96921412c2d236ff5" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==4.62.0" + "version": "==4.62.2" }, "traitlets": { "hashes": [ - "sha256:178f4ce988f69189f7e523337a3e11d91c786ded9360174a3d9ca83e79bc5396", - "sha256:69ff3f9d5351f31a7ad80443c2674b7099df13cc41fc5fa6e2f6d3b0330b0426" + "sha256:03f172516916220b58c9f19d7f854734136dd9528103d04e9bf139a92c9f54c4", + "sha256:bd382d7ea181fbbcce157c133db9a829ce06edffe097bcf3ab945b435452b46d" ], "markers": "python_version >= '3.7'", - "version": "==5.0.5" + "version": "==5.1.0" }, "twine": { "hashes": [ @@ -1528,22 +1604,58 @@ "index": "pypi", "version": "==3.4.2" }, + "typed-ast": { + "hashes": [ + "sha256:01ae5f73431d21eead5015997ab41afa53aa1fbe252f9da060be5dad2c730ace", + "sha256:067a74454df670dcaa4e59349a2e5c81e567d8d65458d480a5b3dfecec08c5ff", + "sha256:0fb71b8c643187d7492c1f8352f2c15b4c4af3f6338f21681d3681b3dc31a266", + "sha256:1b3ead4a96c9101bef08f9f7d1217c096f31667617b58de957f690c92378b528", + "sha256:2068531575a125b87a41802130fa7e29f26c09a2833fea68d9a40cf33902eba6", + "sha256:209596a4ec71d990d71d5e0d312ac935d86930e6eecff6ccc7007fe54d703808", + "sha256:2c726c276d09fc5c414693a2de063f521052d9ea7c240ce553316f70656c84d4", + "sha256:398e44cd480f4d2b7ee8d98385ca104e35c81525dd98c519acff1b79bdaac363", + "sha256:52b1eb8c83f178ab787f3a4283f68258525f8d70f778a2f6dd54d3b5e5fb4341", + "sha256:5feca99c17af94057417d744607b82dd0a664fd5e4ca98061480fd8b14b18d04", + "sha256:7538e495704e2ccda9b234b82423a4038f324f3a10c43bc088a1636180f11a41", + "sha256:760ad187b1041a154f0e4d0f6aae3e40fdb51d6de16e5c99aedadd9246450e9e", + "sha256:777a26c84bea6cd934422ac2e3b78863a37017618b6e5c08f92ef69853e765d3", + "sha256:95431a26309a21874005845c21118c83991c63ea800dd44843e42a916aec5899", + "sha256:9ad2c92ec681e02baf81fdfa056fe0d818645efa9af1f1cd5fd6f1bd2bdfd805", + "sha256:9c6d1a54552b5330bc657b7ef0eae25d00ba7ffe85d9ea8ae6540d2197a3788c", + "sha256:aee0c1256be6c07bd3e1263ff920c325b59849dc95392a05f258bb9b259cf39c", + "sha256:af3d4a73793725138d6b334d9d247ce7e5f084d96284ed23f22ee626a7b88e39", + "sha256:b36b4f3920103a25e1d5d024d155c504080959582b928e91cb608a65c3a49e1a", + "sha256:b9574c6f03f685070d859e75c7f9eeca02d6933273b5e69572e5ff9d5e3931c3", + "sha256:bff6ad71c81b3bba8fa35f0f1921fb24ff4476235a6e94a26ada2e54370e6da7", + "sha256:c190f0899e9f9f8b6b7863debfb739abcb21a5c054f911ca3596d12b8a4c4c7f", + "sha256:c907f561b1e83e93fad565bac5ba9c22d96a54e7ea0267c708bffe863cbe4075", + "sha256:cae53c389825d3b46fb37538441f75d6aecc4174f615d048321b716df2757fb0", + "sha256:dd4a21253f42b8d2b48410cb31fe501d32f8b9fbeb1f55063ad102fe9c425e40", + "sha256:dde816ca9dac1d9c01dd504ea5967821606f02e510438120091b84e852367428", + "sha256:f2362f3cb0f3172c42938946dbc5b7843c2a28aec307c49100c8b38764eb6927", + "sha256:f328adcfebed9f11301eaedfa48e15bdece9b519fb27e6a8c01aa52a17ec31b3", + "sha256:f8afcf15cc511ada719a88e013cec87c11aff7b91f019295eb4530f96fe5ef2f", + "sha256:fb1bbeac803adea29cedd70781399c99138358c26d05fcbd23c13016b7f5ec65" + ], + "markers": "python_version < '3.8' and implementation_name == 'cpython'", + "version": "==1.4.3" + }, "types-requests": { "hashes": [ - "sha256:03122b582f5300ec35ac6692f2634207c467e602dc9ba46b5811a9f6ce0b0bc2", - "sha256:a4c03c654527957a70002079ca48669b53d82eac4811abf140ea93847b65529b" + "sha256:a5a305b43ea57bf64d6731f89816946a405b591eff6de28d4c0fd58422cee779", + "sha256:e21541c0f55c066c491a639309159556dd8c5833e49fcde929c4c47bdb0002ee" ], "index": "pypi", - "version": "==2.25.2" + "version": "==2.25.6" }, "typing-extensions": { "hashes": [ - "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497", - "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342", - "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84" + "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e", + "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7", + "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34" ], "index": "pypi", - "version": "==3.10.0.0" + "version": "==3.10.0.2" }, "urllib3": { "hashes": [ diff --git a/docs/conf.py b/docs/conf.py index 1945f6a9..5488f078 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,7 +21,7 @@ project = "solana.py" copyright = "2020, Michael Huang" # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility.4 -version = "0.12.1" +version = "0.13.0" author = "Michael Huang" diff --git a/setup.py b/setup.py index 680faf6e..4819dc1c 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ setup( name="solana", # *IMPORTANT*: Don't manually change the version here. Use the 'bumpversion' utility. - version="0.12.1", + version="0.13.0", author="Michael Huang", author_mail="michaelhly@gmail.com", description="""Solana.py""", diff --git a/solana/blockhash.py b/solana/blockhash.py index 512210db..cf25eadc 100644 --- a/solana/blockhash.py +++ b/solana/blockhash.py @@ -5,6 +5,53 @@ 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k' """ from typing import NewType +from cachetools import TTLCache Blockhash = NewType("Blockhash", str) """Type for blockhash.""" + + +class BlockhashCache: + """A recent blockhash cache that expires after a given number of seconds.""" + + def __init__(self, ttl: int = 60) -> None: + """Instantiate the cache (you only need to do this once). + + Args: + ---- + ttl (int): Seconds until cached blockhash expires. + + """ + maxsize = 300 + self.unused_blockhashes: TTLCache = TTLCache(maxsize=maxsize, ttl=ttl) + self.used_blockhashes: TTLCache = TTLCache(maxsize=maxsize, ttl=ttl) + + def set(self, blockhash: Blockhash, slot: int) -> None: + """Update the cache. + + Args: + ---- + blockhash (Blockhash): new Blockhash value. + slot (int): the slot which the blockhash came from + + """ + if slot in self.used_blockhashes or slot in self.unused_blockhashes: + return + self.unused_blockhashes[slot] = blockhash + + def get(self) -> Blockhash: + """Get the cached Blockhash. Raises KeyError if cache has expired. + + Returns + ------- + Blockhash: cached Blockhash. + + """ + try: + slot, blockhash = self.unused_blockhashes.popitem() + self.used_blockhashes[slot] = blockhash + except KeyError: + with self.used_blockhashes.timer: # type: ignore + blockhash = self.used_blockhashes[min(self.used_blockhashes)] + # raises ValueError if used_blockhashes is empty + return blockhash diff --git a/solana/rpc/api.py b/solana/rpc/api.py index 2c5349c1..eb770cf0 100644 --- a/solana/rpc/api.py +++ b/solana/rpc/api.py @@ -6,7 +6,7 @@ from warnings import warn from solana.account import Account -from solana.blockhash import Blockhash +from solana.blockhash import Blockhash, BlockhashCache from solana.publickey import PublicKey from solana.rpc import types from solana.transaction import Transaction @@ -34,9 +34,35 @@ def MemcmpOpt(*args, **kwargs) -> types.MemcmpOpts: # pylint: disable=invalid-n class Client(_ClientCore): # pylint: disable=too-many-public-methods """Client class.""" - def __init__(self, endpoint: Optional[str] = None, commitment: Optional[Commitment] = None): - """Init API client.""" - super().__init__(commitment) + def __init__( + self, + endpoint: Optional[str] = None, + commitment: Optional[Commitment] = None, + blockhash_cache: Union[BlockhashCache, bool] = False, + ): + """Init API client. + + :param endpoint: URL of the RPC endpoint. + :param commitment: Default bank state to query. It can be either "finalized", "confirmed" or "processed". + :param blockhash_cache: (Experimental) If True, keep a cache of recent blockhashes to make + `send_transaction` calls faster. + You can also pass your own BlockhashCache object to customize its parameters. + + The cache works as follows: + + 1. Retrieve the oldest unused cached blockhash that is younger than `ttl` seconds, + where `ttl` is defined in the BlockhashCache + (we prefer unused blockhashes because reusing blockhashes can cause errors in some edge cases, + and we prefer slightly older blockhashes because they're more likely to be accepted by every validator). + 2. If there are no unused blockhashes in the cache, take the oldest used + blockhash that is younger than `ttl` seconds. + 3. Fetch a new recent blockhash *after* sending the transaction. This is to keep the cache up-to-date. + + If you want something tailored to your use case, run your own loop that fetches the recent blockhash, + and pass that value in your `.send_transaction` calls. + + """ + super().__init__(commitment, blockhash_cache) self._provider = http.HTTPProvider(endpoint) def is_connected(self) -> bool: @@ -930,13 +956,19 @@ def send_raw_transaction(self, txn: Union[bytes, str], opts: types.TxOpts = type return self.__post_send_with_confirm(*post_send_args) def send_transaction( - self, txn: Transaction, *signers: Account, opts: types.TxOpts = types.TxOpts() + self, + txn: Transaction, + *signers: Account, + opts: types.TxOpts = types.TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> types.RPCResponse: """Send a transaction. :param txn: Transaction object. :param signers: Signers to sign the transaction. :param opts: (optional) Transaction options. + :param recent_blockhash: (optional) Pass a valid recent blockhash here if you want to + skip fetching the recent blockhash or relying on the cache. >>> from solana.account import Account >>> from solana.system_program import TransferParams, transfer @@ -950,17 +982,24 @@ def send_transaction( 'result': '236zSA5w4NaVuLXXHK1mqiBuBxkNBu84X6cfLBh1v6zjPrLfyECz4zdedofBaZFhs4gdwzSmij9VkaSo2tR5LTgG', 'id': 12} """ - try: - # TODO: Cache recent blockhash - blockhash_resp = self.get_recent_blockhash() - if not blockhash_resp["result"]: - raise RuntimeError("failed to get recent blockhash") - txn.recent_blockhash = Blockhash(blockhash_resp["result"]["value"]["blockhash"]) - except Exception as err: - raise RuntimeError("failed to get recent blockhash") from err + if recent_blockhash is None: + if self.blockhash_cache: + try: + recent_blockhash = self.blockhash_cache.get() + except ValueError: + blockhash_resp = self.get_recent_blockhash() + recent_blockhash = self._process_blockhash_resp(blockhash_resp) + else: + blockhash_resp = self.get_recent_blockhash() + recent_blockhash = self.parse_recent_blockhash(blockhash_resp) + txn.recent_blockhash = recent_blockhash txn.sign(*signers) - return self.send_raw_transaction(txn.serialize(), opts=opts) + txn_resp = self.send_raw_transaction(txn.serialize(), opts=opts) + if self.blockhash_cache: + blockhash_resp = self.get_recent_blockhash() + self._process_blockhash_resp(blockhash_resp) + return txn_resp def simulate_transaction( self, txn: Union[bytes, str, Transaction], sig_verify: bool = False, commitment: Optional[Commitment] = None @@ -1037,7 +1076,7 @@ def __confirm_transaction(self, tx_sig: str, commitment: Optional[Commitment] = elapsed_time += sleep_time if not resp["result"]: - raise Exception("Unable to confirm transaction %s" % tx_sig) + raise Exception(f"Unable to confirm transaction {tx_sig}") err = resp.get("error") or resp["result"].get("meta").get("err") if err: self._provider.logger.error("Transaction error: %s", err) diff --git a/solana/rpc/async_api.py b/solana/rpc/async_api.py index 9d418467..d86f1f5f 100644 --- a/solana/rpc/async_api.py +++ b/solana/rpc/async_api.py @@ -3,7 +3,7 @@ from typing import List, Optional, Union from solana.account import Account -from solana.blockhash import Blockhash +from solana.blockhash import Blockhash, BlockhashCache from solana.publickey import PublicKey from solana.rpc import types from solana.transaction import Transaction @@ -16,9 +16,35 @@ class AsyncClient(_ClientCore): # pylint: disable=too-many-public-methods """Async client class.""" - def __init__(self, endpoint: Optional[str] = None, commitment: Optional[Commitment] = None) -> None: - """Init API client.""" - super().__init__(commitment) + def __init__( + self, + endpoint: Optional[str] = None, + commitment: Optional[Commitment] = None, + blockhash_cache: Union[BlockhashCache, bool] = False, + ) -> None: + """Init API client. + + :param endpoint: URL of the RPC endpoint. + :param commitment: Default bank state to query. It can be either "finalized", "confirmed" or "processed". + :param blockhash_cache: (Experimental) If True, keep a cache of recent blockhashes to make + `send_transaction` calls faster. + You can also pass your own BlockhashCache object to customize its parameters. + + The cache works as follows: + + 1. Retrieve the oldest unused cached blockhash that is younger than `ttl` seconds, + where `ttl` is defined in the BlockhashCache + (we prefer unused blockhashes because reusing blockhashes can cause errors in some edge cases, + and we prefer slightly older blockhashes because they're more likely to be accepted by every validator). + 2. If there are no unused blockhashes in the cache, take the oldest used + blockhash that is younger than `ttl` seconds. + 3. Fetch a new recent blockhash *after* sending the transaction. This is to keep the cache up-to-date. + + If you want something tailored to your use case, run your own loop that fetches the recent blockhash, + and pass that value in your `.send_transaction` calls. + + """ + super().__init__(commitment, blockhash_cache) self._provider = async_http.AsyncHTTPProvider(endpoint) async def __aenter__(self) -> "AsyncClient": @@ -929,13 +955,19 @@ async def send_raw_transaction( return await self.__post_send_with_confirm(*post_send_args) async def send_transaction( - self, txn: Transaction, *signers: Account, opts: types.TxOpts = types.TxOpts() + self, + txn: Transaction, + *signers: Account, + opts: types.TxOpts = types.TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> types.RPCResponse: """Send a transaction. :param txn: Transaction object. :param signers: Signers to sign the transaction. :param opts: (optional) Transaction options. + :param recent_blockhash: (optional) Pass a valid recent blockhash here if you want to + skip fetching the recent blockhash or relying on the cache. >>> from solana.account import Account >>> from solana.system_program import TransferParams, transfer @@ -949,17 +981,24 @@ async def send_transaction( 'result': '236zSA5w4NaVuLXXHK1mqiBuBxkNBu84X6cfLBh1v6zjPrLfyECz4zdedofBaZFhs4gdwzSmij9VkaSo2tR5LTgG', 'id': 12} """ - try: - # TODO: Cache recent blockhash - blockhash_resp = await self.get_recent_blockhash() - if not blockhash_resp["result"]: - raise RuntimeError("failed to get recent blockhash") - txn.recent_blockhash = Blockhash(blockhash_resp["result"]["value"]["blockhash"]) - except Exception as err: - raise RuntimeError("failed to get recent blockhash") from err + 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() + recent_blockhash = self._process_blockhash_resp(blockhash_resp) + else: + blockhash_resp = await self.get_recent_blockhash() + recent_blockhash = self.parse_recent_blockhash(blockhash_resp) + txn.recent_blockhash = recent_blockhash txn.sign(*signers) - return await self.send_raw_transaction(txn.serialize(), opts=opts) + txn_resp = await self.send_raw_transaction(txn.serialize(), opts=opts) + if self.blockhash_cache: + blockhash_resp = await self.get_recent_blockhash() + self._process_blockhash_resp(blockhash_resp) + return txn_resp async def simulate_transaction( self, txn: Union[bytes, str, Transaction], sig_verify: bool = False, commitment: Optional[Commitment] = None @@ -1037,7 +1076,7 @@ async def __confirm_transaction(self, tx_sig: str, commitment: Optional[Commitme if not resp["result"]: print(f"resp: {resp}") - raise Exception("Unable to confirm transaction %s" % tx_sig) + raise Exception(f"Unable to confirm transaction {tx_sig}") err = resp.get("error") or resp["result"].get("meta").get("err") if err: self._provider.logger.error("Transaction error: %s", err) diff --git a/solana/rpc/core.py b/solana/rpc/core.py index d595b61a..b480b628 100644 --- a/solana/rpc/core.py +++ b/solana/rpc/core.py @@ -2,12 +2,17 @@ """Helper code for api.py and async_api.py.""" from base64 import b64encode from typing import Any, Dict, List, Optional, Tuple, Union + +try: + from typing import Literal # type: ignore +except ImportError: + from typing_extensions import Literal from warnings import warn from base58 import b58decode, b58encode from solana.account import Account -from solana.blockhash import Blockhash +from solana.blockhash import Blockhash, BlockhashCache from solana.publickey import PublicKey from solana.rpc import types from solana.transaction import Transaction @@ -33,8 +38,11 @@ class _ClientCore: # pylint: disable=too-few-public-methods _get_version = types.RPCMethod("getVersion") _validator_exit = types.RPCMethod("validatorExit") - def __init__(self, commitment: Optional[Commitment] = None): + def __init__(self, commitment: Optional[Commitment] = None, blockhash_cache: Union[BlockhashCache, bool] = False): self._commitment = commitment or Finalized + self.blockhash_cache: Union[BlockhashCache, Literal[False]] = ( + BlockhashCache() if blockhash_cache is True else blockhash_cache + ) def _get_balance_args( self, pubkey: Union[PublicKey, str], commitment: Optional[Commitment] @@ -332,3 +340,17 @@ def _post_send( if not resp.get("result"): raise Exception("Failed to send transaction") return resp + + @staticmethod + def parse_recent_blockhash(blockhash_resp: types.RPCResponse) -> Blockhash: + """Extract blockhash from JSON RPC result.""" + if not blockhash_resp["result"]: + raise RuntimeError("failed to get recent blockhash") + return Blockhash(blockhash_resp["result"]["value"]["blockhash"]) + + def _process_blockhash_resp(self, blockhash_resp: types.RPCResponse) -> Blockhash: + recent_blockhash = self.parse_recent_blockhash(blockhash_resp) + if self.blockhash_cache: + slot = blockhash_resp["result"]["context"]["slot"] + self.blockhash_cache.set(recent_blockhash, slot) + return recent_blockhash diff --git a/spl/token/async_client.py b/spl/token/async_client.py index 53492b83..5461c281 100644 --- a/spl/token/async_client.py +++ b/spl/token/async_client.py @@ -10,6 +10,7 @@ from solana.rpc.async_api import AsyncClient from solana.rpc.commitment import Commitment, Confirmed from solana.rpc.types import RPCResponse, TxOpts +from solana.blockhash import Blockhash from spl.token._layouts import ACCOUNT_LAYOUT, MINT_LAYOUT, MULTISIG_LAYOUT from spl.token.core import AccountInfo, MintInfo, _TokenCore @@ -96,6 +97,7 @@ async def create_mint( program_id: PublicKey, freeze_authority: Optional[PublicKey] = None, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> AsyncToken: """Create and initialize a token. @@ -118,13 +120,14 @@ async def create_mint( conn, payer, mint_authority, decimals, program_id, freeze_authority, skip_confirmation, balance_needed, cls ) # Send the two instructions - await conn.send_transaction(txn, payer, mint_account, opts=opts) + await conn.send_transaction(txn, payer, mint_account, opts=opts, recent_blockhash=recent_blockhash) return cast(AsyncToken, token) async def create_account( self, owner: PublicKey, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: """Create and initialize a new account. @@ -142,13 +145,14 @@ async def create_account( owner, skip_confirmation, balance_needed ) # Send the two instructions - await self._conn.send_transaction(txn, payer, new_account, opts=opts) + await self._conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) return new_account_pk async def create_associated_token_account( self, owner: PublicKey, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: """Create an associated token account. @@ -161,7 +165,7 @@ async def create_associated_token_account( """ # Construct transaction public_key, txn, payer, opts = self._create_associated_token_account_args(owner, skip_confirmation) - await self._conn.send_transaction(txn, payer, opts=opts) + await self._conn.send_transaction(txn, payer, opts=opts, recent_blockhash=recent_blockhash) return public_key @staticmethod @@ -172,6 +176,7 @@ async def create_wrapped_native_account( payer: Account, amount: int, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: """Create and initialize a new account on the special native token mint. @@ -191,7 +196,7 @@ async def create_wrapped_native_account( new_account_public_key, txn, payer, new_account, opts = _TokenCore._create_wrapped_native_account_args( program_id, owner, payer, amount, skip_confirmation, balance_needed ) - await conn.send_transaction(txn, payer, new_account, opts=opts) + await conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) return new_account_public_key async def create_multisig( @@ -199,6 +204,7 @@ async def create_multisig( m: int, multi_signers: List[PublicKey], opts: TxOpts = TxOpts(skip_preflight=True, skip_confirmation=False), + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: # pylint: disable=invalid-name """Create and initialize a new multisig. @@ -208,7 +214,7 @@ async def create_multisig( """ balance_needed = await AsyncToken.get_min_balance_rent_for_exempt_for_multisig(self._conn) txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed) - await self._conn.send_transaction(txn, payer, multisig, opts=opts) + await self._conn.send_transaction(txn, payer, multisig, opts=opts, recent_blockhash=recent_blockhash) return multisig.public_key() async def get_mint_info(self) -> MintInfo: @@ -229,6 +235,7 @@ async def transfer( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Transfer tokens to another account. @@ -240,7 +247,7 @@ async def transfer( :param opts: (optional) Transaction options. """ txn, signers, opts = self._transfer_args(source, dest, owner, amount, multi_signers, opts) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def approve( self, @@ -250,6 +257,7 @@ async def approve( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Grant a third-party permission to transfer up the specified number of tokens from an account. @@ -261,7 +269,7 @@ async def approve( :param opts: (optional) Transaction options. """ txn, payer, signers, opts = self._approve_args(source, delegate, owner, amount, multi_signers, opts) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts) + return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) async def revoke( self, @@ -269,6 +277,7 @@ async def revoke( owner: PublicKey, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Revoke transfer authority for a given account. @@ -278,7 +287,7 @@ async def revoke( :param opts: (optional) Transaction options. """ txn, payer, signers, opts = self._revoke_args(account, owner, multi_signers, opts) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts) + return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) async def set_authority( self, @@ -288,6 +297,7 @@ async def set_authority( new_authority: Optional[PublicKey] = None, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Assign a new authority to the account. @@ -301,7 +311,7 @@ async def set_authority( txn, payer, signers, opts = self._set_authority_args( account, current_authority, authority_type, new_authority, multi_signers, opts ) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts) + return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) async def mint_to( self, @@ -310,6 +320,7 @@ async def mint_to( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Mint new tokens. @@ -323,7 +334,7 @@ async def mint_to( or until the transaction is confirmed. """ txn, signers, opts = self._mint_to_args(dest, mint_authority, amount, multi_signers, opts) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def burn( self, @@ -332,6 +343,7 @@ async def burn( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Burn tokens. @@ -342,7 +354,7 @@ async def burn( :param opts: (optional) Transaction options. """ txn, signers, opts = self._burn_args(account, owner, amount, multi_signers, opts) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def close_account( self, @@ -351,6 +363,7 @@ async def close_account( authority: PublicKey, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Remove approval for the transfer of any remaining tokens. @@ -361,7 +374,7 @@ async def close_account( :param opts: (optional) Transaction options. """ txn, signers, opts = self._close_account_args(account, dest, authority, multi_signers) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def freeze_account( self, @@ -369,6 +382,7 @@ async def freeze_account( authority: PublicKey, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Freeze account. @@ -378,7 +392,7 @@ async def freeze_account( :param opts: (optional) Transaction options. """ txn, signers, opts = self._freeze_account_args(account, authority, multi_signers) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def thaw_account( self, @@ -386,6 +400,7 @@ async def thaw_account( authority: PublicKey, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Thaw account. @@ -395,7 +410,7 @@ async def thaw_account( :param opts: (optional) Transaction options. """ txn, signers, opts = self._thaw_account_args(account, authority, multi_signers) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def transfer_checked( self, @@ -406,6 +421,7 @@ async def transfer_checked( decimals: int, multi_signers: Optional[List[Account]], opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Transfer tokens to another account, asserting the token mint and decimals. @@ -418,7 +434,7 @@ async def transfer_checked( :param opts: (optional) Transaction options. """ txn, signers, opts = self._transfer_checked_args(source, dest, owner, amount, decimals, multi_signers, opts) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def approve_checked( self, @@ -429,6 +445,7 @@ async def approve_checked( decimals: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Grant a third-party permission to transfer up the specified number of tokens from an account. @@ -445,7 +462,7 @@ async def approve_checked( txn, payer, signers, opts = self._approve_checked_args( source, delegate, owner, amount, decimals, multi_signers, opts ) - return await self._conn.send_transaction(txn, payer, *signers, opts=opts) + return await self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) async def mint_to_checked( self, @@ -455,6 +472,7 @@ async def mint_to_checked( decimals: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Mint new tokens, asserting the token mint and decimals. @@ -466,7 +484,7 @@ async def mint_to_checked( :param opts: (optional) Transaction options. """ txn, signers, opts = self._mint_to_checked_args(dest, mint_authority, amount, decimals, multi_signers, opts) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) async def burn_checked( self, @@ -476,6 +494,7 @@ async def burn_checked( decimals: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Burn tokens, asserting the token mint and decimals. @@ -487,4 +506,4 @@ async def burn_checked( :param opts: (optional) Transaction options. """ txn, signers, opts = self._burn_checked_args(account, owner, amount, decimals, multi_signers, opts) - return await self._conn.send_transaction(txn, *signers, opts=opts) + return await self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) diff --git a/spl/token/client.py b/spl/token/client.py index 5222cfdc..26bbbdf0 100644 --- a/spl/token/client.py +++ b/spl/token/client.py @@ -10,6 +10,7 @@ from solana.rpc.api import Client from solana.rpc.commitment import Commitment, Confirmed from solana.rpc.types import RPCResponse, TxOpts +from solana.blockhash import Blockhash from spl.token._layouts import ACCOUNT_LAYOUT, MINT_LAYOUT, MULTISIG_LAYOUT from spl.token.core import AccountInfo, MintInfo, _TokenCore @@ -96,6 +97,7 @@ def create_mint( program_id: PublicKey, freeze_authority: Optional[PublicKey] = None, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> Token: """Create and initialize a token. @@ -118,13 +120,14 @@ def create_mint( conn, payer, mint_authority, decimals, program_id, freeze_authority, skip_confirmation, balance_needed, cls ) # Send the two instructions - conn.send_transaction(txn, payer, mint_account, opts=opts) + conn.send_transaction(txn, payer, mint_account, opts=opts, recent_blockhash=recent_blockhash) return cast(Token, token) def create_account( self, owner: PublicKey, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: """Create and initialize a new account. @@ -142,13 +145,14 @@ def create_account( owner, skip_confirmation, balance_needed ) # Send the two instructions - self._conn.send_transaction(txn, payer, new_account, opts=opts) + self._conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) return new_account_pk def create_associated_token_account( self, owner: PublicKey, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: """Create an associated token account. @@ -161,7 +165,7 @@ def create_associated_token_account( """ # Construct transaction public_key, txn, payer, opts = self._create_associated_token_account_args(owner, skip_confirmation) - self._conn.send_transaction(txn, payer, opts=opts) + self._conn.send_transaction(txn, payer, opts=opts, recent_blockhash=recent_blockhash) return public_key @staticmethod @@ -172,6 +176,7 @@ def create_wrapped_native_account( payer: Account, amount: int, skip_confirmation: bool = False, + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: """Create and initialize a new account on the special native token mint. @@ -191,7 +196,7 @@ def create_wrapped_native_account( new_account_public_key, txn, payer, new_account, opts = _TokenCore._create_wrapped_native_account_args( program_id, owner, payer, amount, skip_confirmation, balance_needed ) - conn.send_transaction(txn, payer, new_account, opts=opts) + conn.send_transaction(txn, payer, new_account, opts=opts, recent_blockhash=recent_blockhash) return new_account_public_key def create_multisig( @@ -199,6 +204,7 @@ def create_multisig( m: int, multi_signers: List[PublicKey], opts: TxOpts = TxOpts(skip_preflight=True, skip_confirmation=False), + recent_blockhash: Optional[Blockhash] = None, ) -> PublicKey: # pylint: disable=invalid-name """Create and initialize a new multisig. @@ -208,7 +214,7 @@ def create_multisig( """ balance_needed = Token.get_min_balance_rent_for_exempt_for_multisig(self._conn) txn, payer, multisig = self._create_multisig_args(m, multi_signers, balance_needed) - self._conn.send_transaction(txn, payer, multisig, opts=opts) + self._conn.send_transaction(txn, payer, multisig, opts=opts, recent_blockhash=recent_blockhash) return multisig.public_key() def get_mint_info(self) -> MintInfo: @@ -229,6 +235,7 @@ def transfer( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Transfer tokens to another account. @@ -240,7 +247,7 @@ def transfer( :param opts: (optional) Transaction options. """ txn, signers, opts = self._transfer_args(source, dest, owner, amount, multi_signers, opts) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def approve( self, @@ -250,6 +257,7 @@ def approve( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Grant a third-party permission to transfer up the specified number of tokens from an account. @@ -261,7 +269,7 @@ def approve( :param opts: (optional) Transaction options. """ txn, payer, signers, opts = self._approve_args(source, delegate, owner, amount, multi_signers, opts) - return self._conn.send_transaction(txn, payer, *signers, opts=opts) + return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) def revoke( self, @@ -269,6 +277,7 @@ def revoke( owner: PublicKey, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Revoke transfer authority for a given account. @@ -278,7 +287,7 @@ def revoke( :param opts: (optional) Transaction options. """ txn, payer, signers, opts = self._revoke_args(account, owner, multi_signers, opts) - return self._conn.send_transaction(txn, payer, *signers, opts=opts) + return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) def set_authority( self, @@ -288,6 +297,7 @@ def set_authority( new_authority: Optional[PublicKey] = None, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Assign a new authority to the account. @@ -301,7 +311,7 @@ def set_authority( txn, payer, signers, opts = self._set_authority_args( account, current_authority, authority_type, new_authority, multi_signers, opts ) - return self._conn.send_transaction(txn, payer, *signers, opts=opts) + return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) def mint_to( self, @@ -310,6 +320,7 @@ def mint_to( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Mint new tokens. @@ -323,7 +334,7 @@ def mint_to( or until the transaction is confirmed. """ txn, signers, opts = self._mint_to_args(dest, mint_authority, amount, multi_signers, opts) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def burn( self, @@ -332,6 +343,7 @@ def burn( amount: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Burn tokens. @@ -342,7 +354,7 @@ def burn( :param opts: (optional) Transaction options. """ txn, signers, opts = self._burn_args(account, owner, amount, multi_signers, opts) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def close_account( self, @@ -351,6 +363,7 @@ def close_account( authority: Union[Account, PublicKey], multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Remove approval for the transfer of any remaining tokens. @@ -361,7 +374,7 @@ def close_account( :param opts: (optional) Transaction options. """ txn, signers, opts = self._close_account_args(account, dest, authority, multi_signers) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def freeze_account( self, @@ -369,6 +382,7 @@ def freeze_account( authority: Union[PublicKey, Account], multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Freeze account. @@ -378,7 +392,7 @@ def freeze_account( :param opts: (optional) Transaction options. """ txn, signers, opts = self._freeze_account_args(account, authority, multi_signers) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def thaw_account( self, @@ -386,6 +400,7 @@ def thaw_account( authority: PublicKey, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Thaw account. @@ -395,7 +410,7 @@ def thaw_account( :param opts: (optional) Transaction options. """ txn, signers, opts = self._thaw_account_args(account, authority, multi_signers) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def transfer_checked( self, @@ -406,6 +421,7 @@ def transfer_checked( decimals: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Transfer tokens to another account, asserting the token mint and decimals. @@ -418,7 +434,7 @@ def transfer_checked( :param opts: (optional) Transaction options. """ txn, signers, opts = self._transfer_checked_args(source, dest, owner, amount, decimals, multi_signers, opts) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def approve_checked( self, @@ -429,6 +445,7 @@ def approve_checked( decimals: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Grant a third-party permission to transfer up the specified number of tokens from an account. @@ -445,7 +462,7 @@ def approve_checked( txn, payer, signers, opts = self._approve_checked_args( source, delegate, owner, amount, decimals, multi_signers, opts ) - return self._conn.send_transaction(txn, payer, *signers, opts=opts) + return self._conn.send_transaction(txn, payer, *signers, opts=opts, recent_blockhash=recent_blockhash) def mint_to_checked( self, @@ -455,6 +472,7 @@ def mint_to_checked( decimals: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Mint new tokens, asserting the token mint and decimals. @@ -466,7 +484,7 @@ def mint_to_checked( :param opts: (optional) Transaction options. """ txn, signers, opts = self._mint_to_checked_args(dest, mint_authority, amount, decimals, multi_signers, opts) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) def burn_checked( self, @@ -476,6 +494,7 @@ def burn_checked( decimals: int, multi_signers: Optional[List[Account]] = None, opts: TxOpts = TxOpts(), + recent_blockhash: Optional[Blockhash] = None, ) -> RPCResponse: """Burn tokens, asserting the token mint and decimals. @@ -487,4 +506,4 @@ def burn_checked( :param opts: (optional) Transaction options. """ txn, signers, opts = self._burn_checked_args(account, owner, amount, decimals, multi_signers, opts) - return self._conn.send_transaction(txn, *signers, opts=opts) + return self._conn.send_transaction(txn, *signers, opts=opts, recent_blockhash=recent_blockhash) diff --git a/tests/conftest.py b/tests/conftest.py index b5977375..9aa06867 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,11 +40,35 @@ def stubbed_receiver() -> PublicKey: @pytest.fixture(scope="session") -def alt_stubbed_receiver() -> PublicKey: +def stubbed_receiver_prefetched_blockhash() -> PublicKey: + """Arbitrary known public key to be used as reciever.""" + return PublicKey("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i97") + + +@pytest.fixture(scope="session") +def stubbed_receiver_cached_blockhash() -> PublicKey: + """Arbitrary known public key to be used as reciever.""" + return PublicKey("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i95") + + +@pytest.fixture(scope="session") +def async_stubbed_receiver() -> PublicKey: """Arbitrary known public key to be used as reciever.""" return PublicKey("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i98") +@pytest.fixture(scope="session") +def async_stubbed_receiver_prefetched_blockhash() -> PublicKey: + """Arbitrary known public key to be used as reciever.""" + return PublicKey("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i96") + + +@pytest.fixture(scope="session") +def async_stubbed_receiver_cached_blockhash() -> PublicKey: + """Arbitrary known public key to be used as reciever.""" + return PublicKey("J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i94") + + @pytest.fixture(scope="session") def stubbed_sender() -> Account: """Arbitrary known account to be used as sender.""" @@ -52,11 +76,41 @@ def stubbed_sender() -> Account: @pytest.fixture(scope="session") -def alt_stubbed_sender() -> Account: +def stubbed_sender_prefetched_blockhash() -> Account: + """Arbitrary known account to be used as sender.""" + return Account(bytes([9] * PublicKey.LENGTH)) + + +@pytest.fixture(scope="session") +def stubbed_sender_cached_blockhash() -> Account: + """Arbitrary known account to be used as sender.""" + return Account(bytes([4] * PublicKey.LENGTH)) + + +@pytest.fixture(scope="session") +def stubbed_sender_for_token() -> Account: + """Arbitrary known account to be used as sender.""" + return Account(bytes([2] * PublicKey.LENGTH)) + + +@pytest.fixture(scope="session") +def async_stubbed_sender() -> Account: """Another arbitrary known account to be used as sender.""" return Account(bytes([7] * PublicKey.LENGTH)) +@pytest.fixture(scope="session") +def async_stubbed_sender_prefetched_blockhash() -> Account: + """Another arbitrary known account to be used as sender.""" + return Account(bytes([5] * PublicKey.LENGTH)) + + +@pytest.fixture(scope="session") +def async_stubbed_sender_cached_blockhash() -> Account: + """Another arbitrary known account to be used as sender.""" + return Account(bytes([3] * PublicKey.LENGTH)) + + @pytest.fixture(scope="session") def freeze_authority() -> Account: """Arbitrary known account to be used as freeze authority.""" @@ -72,6 +126,15 @@ def test_http_client(docker_services) -> Client: return http_client +@pytest.mark.integration +@pytest.fixture(scope="session") +def test_http_client_cached_blockhash(docker_services) -> Client: + """Test http_client.is_connected.""" + http_client = Client(blockhash_cache=True) + docker_services.wait_until_responsive(timeout=15, pause=1, check=http_client.is_connected) + return http_client + + @pytest.mark.integration @pytest.fixture(scope="session") def test_http_client_async(docker_services, event_loop) -> AsyncClient: # pylint: disable=redefined-outer-name @@ -88,19 +151,15 @@ def check() -> bool: @pytest.mark.integration @pytest.fixture(scope="session") -def test_http_clients(docker_services) -> Clients: +def test_http_client_async_cached_blockhash( + docker_services, event_loop # pylint: disable=redefined-outer-name +) -> AsyncClient: """Test http_client.is_connected.""" - http_client = Client() - async_client = AsyncClient() - loop = asyncio.get_event_loop() + http_client = AsyncClient(blockhash_cache=True) def check() -> bool: - sync_result = http_client.is_connected() - async_result = loop.run_until_complete(async_client.is_connected()) - return sync_result and async_result + return event_loop.run_until_complete(http_client.is_connected()) docker_services.wait_until_responsive(timeout=15, pause=1, check=check) - clients = Clients(sync=http_client, async_=async_client, loop=loop) - yield clients - - clients.loop.run_until_complete(async_client.close()) + yield http_client + event_loop.run_until_complete(http_client.close()) diff --git a/tests/integration/test_async_http_client.py b/tests/integration/test_async_http_client.py index 23ad033e..c7338435 100644 --- a/tests/integration/test_async_http_client.py +++ b/tests/integration/test_async_http_client.py @@ -10,9 +10,9 @@ @pytest.mark.integration @pytest.mark.asyncio -async def test_request_air_drop(alt_stubbed_sender, test_http_client_async): - """Test air drop to alt_stubbed_sender.""" - resp = await test_http_client_async.request_airdrop(alt_stubbed_sender.public_key(), AIRDROP_AMOUNT) +async def test_request_air_drop(async_stubbed_sender, test_http_client_async): + """Test air drop to async_stubbed_sender.""" + resp = await test_http_client_async.request_airdrop(async_stubbed_sender.public_key(), AIRDROP_AMOUNT) assert_valid_response(resp) resp = await aconfirm_transaction(test_http_client_async, resp["result"]) assert_valid_response(resp) @@ -22,17 +22,47 @@ async def test_request_air_drop(alt_stubbed_sender, test_http_client_async): @pytest.mark.integration @pytest.mark.asyncio -async def test_send_transaction_and_get_balance(alt_stubbed_sender, alt_stubbed_receiver, test_http_client_async): +async def test_request_air_drop_prefetched_blockhash(async_stubbed_sender_prefetched_blockhash, test_http_client_async): + """Test air drop to async_stubbed_sender.""" + resp = await test_http_client_async.request_airdrop( + async_stubbed_sender_prefetched_blockhash.public_key(), AIRDROP_AMOUNT + ) + assert_valid_response(resp) + resp = await aconfirm_transaction(test_http_client_async, resp["result"]) + assert_valid_response(resp) + expected_meta = generate_expected_meta_after_airdrop(resp) + assert resp["result"]["meta"] == expected_meta + + +@pytest.mark.integration +@pytest.mark.asyncio +async def test_request_air_drop_cached_blockhash( + async_stubbed_sender_cached_blockhash, test_http_client_async_cached_blockhash +): + """Test air drop to async_stubbed_sender.""" + resp = await test_http_client_async_cached_blockhash.request_airdrop( + async_stubbed_sender_cached_blockhash.public_key(), AIRDROP_AMOUNT + ) + assert_valid_response(resp) + resp = await aconfirm_transaction(test_http_client_async_cached_blockhash, resp["result"]) + assert_valid_response(resp) + expected_meta = generate_expected_meta_after_airdrop(resp) + assert resp["result"]["meta"] == expected_meta + + +@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 alt_stubbed_receiver + # Create transfer tx to transfer lamports from stubbed sender to async_stubbed_receiver transfer_tx = Transaction().add( sp.transfer( sp.TransferParams( - from_pubkey=alt_stubbed_sender.public_key(), to_pubkey=alt_stubbed_receiver, lamports=1000 + from_pubkey=async_stubbed_sender.public_key(), to_pubkey=async_stubbed_receiver, lamports=1000 ) ) ) - resp = await test_http_client_async.send_transaction(transfer_tx, alt_stubbed_sender) + resp = await test_http_client_async.send_transaction(transfer_tx, async_stubbed_sender) assert_valid_response(resp) # Confirm transaction resp = await aconfirm_transaction(test_http_client_async, resp["result"]) @@ -62,32 +92,187 @@ async def test_send_transaction_and_get_balance(alt_stubbed_sender, alt_stubbed_ } assert resp["result"]["meta"] == expected_meta # Check balances - resp = await test_http_client_async.get_balance(alt_stubbed_sender.public_key()) + resp = await test_http_client_async.get_balance(async_stubbed_sender.public_key()) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999994000 + resp = await test_http_client_async.get_balance(async_stubbed_receiver) + assert_valid_response(resp) + assert resp["result"]["value"] == 954 + + +@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 +): + """Test sending a transaction to localnet.""" + # Create transfer tx to transfer lamports from stubbed sender to async_stubbed_receiver + transfer_tx = Transaction().add( + sp.transfer( + sp.TransferParams( + from_pubkey=async_stubbed_sender_prefetched_blockhash.public_key(), + to_pubkey=async_stubbed_receiver_prefetched_blockhash, + lamports=1000, + ) + ) + ) + resp = await test_http_client_async.send_transaction(transfer_tx, async_stubbed_sender_prefetched_blockhash) + assert_valid_response(resp) + # Confirm transaction + resp = await aconfirm_transaction(test_http_client_async, resp["result"]) + assert_valid_response(resp) + expected_meta = { + "err": None, + "fee": 5000, + "innerInstructions": [], + "logMessages": [ + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + ], + "postBalances": [9999994000, 954, 1], + "postTokenBalances": [], + "preBalances": [10000000000, 0, 1], + "preTokenBalances": [], + "rewards": [ + { + "commission": None, + "lamports": -46, + "postBalance": 954, + "pubkey": "J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i96", + "rewardType": "Rent", + } + ], + "status": {"Ok": None}, + } + assert resp["result"]["meta"] == expected_meta + # Check balances + resp = await test_http_client_async.get_balance(async_stubbed_sender_prefetched_blockhash.public_key()) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999994000 + resp = await test_http_client_async.get_balance(async_stubbed_receiver_prefetched_blockhash) + assert_valid_response(resp) + assert resp["result"]["value"] == 954 + + +@pytest.mark.integration +@pytest.mark.asyncio +async def test_send_transaction_cached_blockhash( + async_stubbed_sender_cached_blockhash, + async_stubbed_receiver_cached_blockhash, + test_http_client_async_cached_blockhash, +): + """Test sending a transaction to localnet.""" + # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction().add( + sp.transfer( + sp.TransferParams( + from_pubkey=async_stubbed_sender_cached_blockhash.public_key(), + to_pubkey=async_stubbed_receiver_cached_blockhash, + lamports=1000, + ) + ) + ) + resp = await test_http_client_async_cached_blockhash.send_transaction( + transfer_tx, async_stubbed_sender_cached_blockhash + ) + assert_valid_response(resp) + # Confirm transaction + resp = await aconfirm_transaction(test_http_client_async_cached_blockhash, resp["result"]) + assert_valid_response(resp) + expected_meta = { + "err": None, + "fee": 5000, + "innerInstructions": [], + "logMessages": [ + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + ], + "postBalances": [9999994000, 954, 1], + "postTokenBalances": [], + "preBalances": [10000000000, 0, 1], + "preTokenBalances": [], + "rewards": [ + { + "commission": None, + "lamports": -46, + "postBalance": 954, + "pubkey": "J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i94", + "rewardType": "Rent", + } + ], + "status": {"Ok": None}, + } + assert resp["result"]["meta"] == expected_meta + # Check balances + resp = await test_http_client_async_cached_blockhash.get_balance(async_stubbed_sender_cached_blockhash.public_key()) assert_valid_response(resp) assert resp["result"]["value"] == 9999994000 - resp = await test_http_client_async.get_balance(alt_stubbed_receiver) + assert len(test_http_client_async_cached_blockhash.blockhash_cache.unused_blockhashes) == 1 + + # Second transaction + transfer_tx = Transaction().add( + sp.transfer( + sp.TransferParams( + from_pubkey=async_stubbed_sender_cached_blockhash.public_key(), + to_pubkey=async_stubbed_receiver_cached_blockhash, + lamports=2000, + ) + ) + ) + resp = await test_http_client_async_cached_blockhash.get_balance(async_stubbed_receiver_cached_blockhash) assert_valid_response(resp) assert resp["result"]["value"] == 954 + resp = await test_http_client_async_cached_blockhash.send_transaction( + transfer_tx, async_stubbed_sender_cached_blockhash + ) + assert_valid_response(resp) + # Confirm transaction + resp = await aconfirm_transaction(test_http_client_async_cached_blockhash, resp["result"]) + assert_valid_response(resp) + expected_meta = { + "err": None, + "fee": 5000, + "innerInstructions": [], + "logMessages": [ + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + ], + "postBalances": [9999987000, 2954, 1], + "postTokenBalances": [], + "preBalances": [9999994000, 954, 1], + "preTokenBalances": [], + "rewards": [], + "status": {"Ok": None}, + } + assert resp["result"]["meta"] == expected_meta + # Check balances + resp = await test_http_client_async_cached_blockhash.get_balance(async_stubbed_sender_cached_blockhash.public_key()) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999987000 + assert len(test_http_client_async_cached_blockhash.blockhash_cache.unused_blockhashes) == 1 + assert len(test_http_client_async_cached_blockhash.blockhash_cache.used_blockhashes) == 1 @pytest.mark.integration @pytest.mark.asyncio -async def test_send_raw_transaction_and_get_balance(alt_stubbed_sender, alt_stubbed_receiver, test_http_client_async): +async def test_send_raw_transaction_and_get_balance( + async_stubbed_sender, async_stubbed_receiver, test_http_client_async +): """Test sending a raw transaction to localnet.""" # Get a recent blockhash resp = await test_http_client_async.get_recent_blockhash() assert_valid_response(resp) recent_blockhash = resp["result"]["value"]["blockhash"] - # Create transfer tx transfer lamports from stubbed sender to alt_stubbed_receiver + # 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=alt_stubbed_sender.public_key(), to_pubkey=alt_stubbed_receiver, lamports=1000 + from_pubkey=async_stubbed_sender.public_key(), to_pubkey=async_stubbed_receiver, lamports=1000 ) ) ) # Sign transaction - transfer_tx.sign(alt_stubbed_sender) + transfer_tx.sign(async_stubbed_sender) # Send raw transaction resp = await test_http_client_async.send_raw_transaction(transfer_tx.serialize()) assert_valid_response(resp) @@ -111,10 +296,10 @@ async def test_send_raw_transaction_and_get_balance(alt_stubbed_sender, alt_stub } assert resp["result"]["meta"] == expected_meta # Check balances - resp = await test_http_client_async.get_balance(alt_stubbed_sender.public_key()) + resp = await test_http_client_async.get_balance(async_stubbed_sender.public_key()) assert_valid_response(resp) assert resp["result"]["value"] == 9999988000 - resp = await test_http_client_async.get_balance(alt_stubbed_receiver) + resp = await test_http_client_async.get_balance(async_stubbed_receiver) assert_valid_response(resp) assert resp["result"]["value"] == 1954 @@ -328,13 +513,15 @@ async def test_get_version(test_http_client_async): @pytest.mark.integration @pytest.mark.asyncio -async def test_get_account_info(alt_stubbed_sender, test_http_client_async): +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(alt_stubbed_sender.public_key()) + resp = await test_http_client_async.get_account_info(async_stubbed_sender.public_key()) assert_valid_response(resp) - resp = await test_http_client_async.get_account_info(alt_stubbed_sender.public_key(), encoding="jsonParsed") + resp = await test_http_client_async.get_account_info(async_stubbed_sender.public_key(), encoding="jsonParsed") assert_valid_response(resp) - resp = await test_http_client_async.get_account_info(alt_stubbed_sender.public_key(), data_slice=DataSliceOpt(1, 1)) + resp = await test_http_client_async.get_account_info( + async_stubbed_sender.public_key(), data_slice=DataSliceOpt(1, 1) + ) assert_valid_response(resp) diff --git a/tests/integration/test_async_token_client.py b/tests/integration/test_async_token_client.py index 0bcdee01..1675c993 100644 --- a/tests/integration/test_async_token_client.py +++ b/tests/integration/test_async_token_client.py @@ -15,17 +15,17 @@ @pytest.mark.integration @pytest.mark.asyncio @pytest.fixture(scope="module") -async def test_token(alt_stubbed_sender, freeze_authority, test_http_client_async) -> AsyncToken: +async def test_token(async_stubbed_sender, freeze_authority, test_http_client_async) -> AsyncToken: """Test create mint.""" - resp = await test_http_client_async.request_airdrop(alt_stubbed_sender.public_key(), AIRDROP_AMOUNT) + resp = await test_http_client_async.request_airdrop(async_stubbed_sender.public_key(), AIRDROP_AMOUNT) confirmed = await aconfirm_transaction(test_http_client_async, resp["result"]) assert_valid_response(confirmed) expected_decimals = 6 token_client = await AsyncToken.create_mint( test_http_client_async, - alt_stubbed_sender, - alt_stubbed_sender.public_key(), + async_stubbed_sender, + async_stubbed_sender.public_key(), expected_decimals, TOKEN_PROGRAM_ID, freeze_authority.public_key(), @@ -33,7 +33,7 @@ async def test_token(alt_stubbed_sender, freeze_authority, test_http_client_asyn assert token_client.pubkey assert token_client.program_id == TOKEN_PROGRAM_ID - assert token_client.payer.public_key() == alt_stubbed_sender.public_key() + assert token_client.payer.public_key() == async_stubbed_sender.public_key() resp = await test_http_client_async.get_account_info(token_client.pubkey) assert_valid_response(resp) @@ -43,7 +43,7 @@ async def test_token(alt_stubbed_sender, freeze_authority, test_http_client_asyn assert mint_data.is_initialized assert mint_data.decimals == expected_decimals assert mint_data.supply == 0 - assert PublicKey(mint_data.mint_authority) == alt_stubbed_sender.public_key() + assert PublicKey(mint_data.mint_authority) == async_stubbed_sender.public_key() assert PublicKey(mint_data.freeze_authority) == freeze_authority.public_key() return token_client @@ -51,30 +51,30 @@ async def test_token(alt_stubbed_sender, freeze_authority, test_http_client_asyn @pytest.mark.integration @pytest.mark.asyncio @pytest.fixture(scope="module") -async def alt_stubbed_sender_token_account_pk( - alt_stubbed_sender, test_token # pylint: disable=redefined-outer-name +async def async_stubbed_sender_token_account_pk( + async_stubbed_sender, test_token # pylint: disable=redefined-outer-name ) -> PublicKey: """Token account for stubbed sender.""" - return await test_token.create_account(alt_stubbed_sender.public_key()) + return await test_token.create_account(async_stubbed_sender.public_key()) @pytest.mark.integration @pytest.mark.asyncio @pytest.fixture(scope="module") -async def alt_stubbed_receiver_token_account_pk( - alt_stubbed_receiver, test_token # pylint: disable=redefined-outer-name +async def async_stubbed_receiver_token_account_pk( + async_stubbed_receiver, test_token # pylint: disable=redefined-outer-name ) -> PublicKey: """Token account for stubbed reciever.""" - return await test_token.create_account(alt_stubbed_receiver) + return await test_token.create_account(async_stubbed_receiver) @pytest.mark.integration @pytest.mark.asyncio async def test_new_account( - alt_stubbed_sender, test_http_client_async, test_token + async_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(alt_stubbed_sender.public_key()) + token_account_pk = await test_token.create_account(async_stubbed_sender.public_key()) resp = await test_http_client_async.get_account_info(token_account_pk) assert_valid_response(resp) assert resp["result"]["value"]["owner"] == str(TOKEN_PROGRAM_ID) @@ -90,7 +90,7 @@ async def test_new_account( assert not account_data.close_authority_option and PublicKey(account_data.close_authority) == PublicKey(0) assert not account_data.is_native_option and not account_data.is_native assert PublicKey(account_data.mint) == test_token.pubkey - assert PublicKey(account_data.owner) == alt_stubbed_sender.public_key() + assert PublicKey(account_data.owner) == async_stubbed_sender.public_key() @pytest.mark.integration @@ -109,13 +109,13 @@ async def test_new_associated_account(test_token): # pylint: disable=redefined- @pytest.mark.integration @pytest.mark.asyncio async def test_get_account_info( - alt_stubbed_sender, alt_stubbed_sender_token_account_pk, test_token + async_stubbed_sender, async_stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name """Test get token account info.""" - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) assert account_info.is_initialized is True assert account_info.mint == test_token.pubkey - assert account_info.owner == alt_stubbed_sender.public_key() + assert account_info.owner == async_stubbed_sender.public_key() assert account_info.amount == 0 assert account_info.delegate is None assert account_info.delegated_amount == 0 @@ -127,10 +127,12 @@ async def test_get_account_info( @pytest.mark.integration @pytest.mark.asyncio -async def test_get_mint_info(alt_stubbed_sender, freeze_authority, test_token): # pylint: disable=redefined-outer-name +async def test_get_mint_info( + async_stubbed_sender, freeze_authority, test_token +): # pylint: disable=redefined-outer-name """Test get token mint info.""" mint_info = await test_token.get_mint_info() - assert mint_info.mint_authority == alt_stubbed_sender.public_key() + assert mint_info.mint_authority == async_stubbed_sender.public_key() assert mint_info.supply == 0 assert mint_info.decimals == 6 assert mint_info.is_initialized is True @@ -140,18 +142,18 @@ async def test_get_mint_info(alt_stubbed_sender, freeze_authority, test_token): @pytest.mark.integration @pytest.mark.asyncio async def test_mint_to( - alt_stubbed_sender, alt_stubbed_sender_token_account_pk, test_token + async_stubbed_sender, async_stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name """Test mint token to account and get balance.""" expected_amount = 1000 resp = await test_token.mint_to( - dest=alt_stubbed_sender_token_account_pk, - mint_authority=alt_stubbed_sender, + dest=async_stubbed_sender_token_account_pk, + mint_authority=async_stubbed_sender, amount=1000, opts=TxOpts(skip_confirmation=False), ) assert_valid_response(resp) - resp = await test_token.get_balance(alt_stubbed_sender_token_account_pk) + resp = await test_token.get_balance(async_stubbed_sender_token_account_pk) balance_info = resp["result"]["value"] assert balance_info["amount"] == str(expected_amount) assert balance_info["decimals"] == 6 @@ -161,19 +163,19 @@ async def test_mint_to( @pytest.mark.integration @pytest.mark.asyncio async def test_transfer( - alt_stubbed_sender, alt_stubbed_receiver_token_account_pk, alt_stubbed_sender_token_account_pk, test_token + async_stubbed_sender, async_stubbed_receiver_token_account_pk, async_stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name """Test token transfer.""" expected_amount = 500 resp = await test_token.transfer( - source=alt_stubbed_sender_token_account_pk, - dest=alt_stubbed_receiver_token_account_pk, - owner=alt_stubbed_sender, + source=async_stubbed_sender_token_account_pk, + dest=async_stubbed_receiver_token_account_pk, + owner=async_stubbed_sender, amount=expected_amount, opts=TxOpts(skip_confirmation=False), ) assert_valid_response(resp) - resp = await test_token.get_balance(alt_stubbed_receiver_token_account_pk) + resp = await test_token.get_balance(async_stubbed_receiver_token_account_pk) balance_info = resp["result"]["value"] assert balance_info["amount"] == str(expected_amount) assert balance_info["decimals"] == 6 @@ -183,22 +185,22 @@ async def test_transfer( @pytest.mark.integration @pytest.mark.asyncio async def test_burn( - alt_stubbed_sender, alt_stubbed_sender_token_account_pk, test_token + async_stubbed_sender, async_stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name """Test burning tokens.""" burn_amount = 200 expected_amount = 300 burn_resp = await test_token.burn( - account=alt_stubbed_sender_token_account_pk, - owner=alt_stubbed_sender, + account=async_stubbed_sender_token_account_pk, + owner=async_stubbed_sender, amount=burn_amount, multi_signers=None, opts=TxOpts(skip_confirmation=False), ) assert_valid_response(burn_resp) - resp = await test_token.get_balance(alt_stubbed_sender_token_account_pk) + resp = await test_token.get_balance(async_stubbed_sender_token_account_pk) balance_info = resp["result"]["value"] assert balance_info["amount"] == str(expected_amount) assert balance_info["decimals"] == 6 @@ -208,8 +210,8 @@ async def test_burn( @pytest.mark.integration @pytest.mark.asyncio async def test_mint_to_checked( - alt_stubbed_sender, - alt_stubbed_sender_token_account_pk, + async_stubbed_sender, + async_stubbed_sender_token_account_pk, test_token, ): # pylint: disable=redefined-outer-name """Test mint token checked and get balance.""" @@ -218,8 +220,8 @@ async def test_mint_to_checked( expected_decimals = 6 mint_resp = await test_token.mint_to_checked( - dest=alt_stubbed_sender_token_account_pk, - mint_authority=alt_stubbed_sender, + dest=async_stubbed_sender_token_account_pk, + mint_authority=async_stubbed_sender, amount=mint_amount, decimals=expected_decimals, multi_signers=None, @@ -227,7 +229,7 @@ async def test_mint_to_checked( ) assert_valid_response(mint_resp) - resp = await test_token.get_balance(alt_stubbed_sender_token_account_pk) + resp = await test_token.get_balance(async_stubbed_sender_token_account_pk) balance_info = resp["result"]["value"] assert balance_info["amount"] == str(expected_amount) assert balance_info["decimals"] == expected_decimals @@ -237,7 +239,7 @@ async def test_mint_to_checked( @pytest.mark.integration @pytest.mark.asyncio async def test_transfer_checked( - alt_stubbed_sender, alt_stubbed_receiver_token_account_pk, alt_stubbed_sender_token_account_pk, test_token + async_stubbed_sender, async_stubbed_receiver_token_account_pk, async_stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name """Test token transfer.""" transfer_amount = 500 @@ -245,9 +247,9 @@ async def test_transfer_checked( expected_decimals = 6 transfer_resp = await test_token.transfer_checked( - source=alt_stubbed_sender_token_account_pk, - dest=alt_stubbed_receiver_token_account_pk, - owner=alt_stubbed_sender, + source=async_stubbed_sender_token_account_pk, + dest=async_stubbed_receiver_token_account_pk, + owner=async_stubbed_sender, amount=transfer_amount, decimals=expected_decimals, multi_signers=None, @@ -255,7 +257,7 @@ async def test_transfer_checked( ) assert_valid_response(transfer_resp) - resp = await test_token.get_balance(alt_stubbed_receiver_token_account_pk) + resp = await test_token.get_balance(async_stubbed_receiver_token_account_pk) balance_info = resp["result"]["value"] assert balance_info["amount"] == str(total_amount) assert balance_info["decimals"] == expected_decimals @@ -265,15 +267,15 @@ async def test_transfer_checked( @pytest.mark.integration @pytest.mark.asyncio async def test_burn_checked( - alt_stubbed_sender, alt_stubbed_sender_token_account_pk, test_token + async_stubbed_sender, async_stubbed_sender_token_account_pk, test_token ): # pylint: disable=redefined-outer-name """Test burning tokens checked.""" burn_amount = 500 expected_decimals = 6 burn_resp = await test_token.burn_checked( - account=alt_stubbed_sender_token_account_pk, - owner=alt_stubbed_sender, + account=async_stubbed_sender_token_account_pk, + owner=async_stubbed_sender, amount=burn_amount, decimals=expected_decimals, multi_signers=None, @@ -281,7 +283,7 @@ async def test_burn_checked( ) assert_valid_response(burn_resp) - resp = await test_token.get_balance(alt_stubbed_sender_token_account_pk) + resp = await test_token.get_balance(async_stubbed_sender_token_account_pk) balance_info = resp["result"]["value"] assert balance_info["amount"] == str(0) assert balance_info["decimals"] == expected_decimals @@ -290,55 +292,63 @@ async def test_burn_checked( @pytest.mark.integration @pytest.mark.asyncio -async def test_get_accounts(alt_stubbed_sender, test_token): # pylint: disable=redefined-outer-name +async def test_get_accounts(async_stubbed_sender, test_token): # pylint: disable=redefined-outer-name """Test get token accounts.""" - resp = await test_token.get_accounts(alt_stubbed_sender.public_key()) + resp = await test_token.get_accounts(async_stubbed_sender.public_key()) assert_valid_response(resp) assert len(resp["result"]["value"]) == 2 for resp_data in resp["result"]["value"]: assert PublicKey(resp_data["pubkey"]) parsed_data = resp_data["account"]["data"]["parsed"]["info"] - assert parsed_data["owner"] == str(alt_stubbed_sender.public_key()) + assert parsed_data["owner"] == str(async_stubbed_sender.public_key()) @pytest.mark.integration @pytest.mark.asyncio async def test_approve( - alt_stubbed_sender, alt_stubbed_receiver, alt_stubbed_sender_token_account_pk, test_token, test_http_client_async + async_stubbed_sender, + async_stubbed_receiver, + async_stubbed_sender_token_account_pk, + test_token, + test_http_client_async, ): # pylint: disable=redefined-outer-name """Test approval for delgating a token account.""" expected_amount_delegated = 500 resp = await test_token.approve( - source=alt_stubbed_sender_token_account_pk, - delegate=alt_stubbed_receiver, - owner=alt_stubbed_sender.public_key(), + source=async_stubbed_sender_token_account_pk, + delegate=async_stubbed_receiver, + owner=async_stubbed_sender.public_key(), amount=expected_amount_delegated, ) confirmed = await aconfirm_transaction(test_http_client_async, resp["result"]) assert_valid_response(confirmed) - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) - assert account_info.delegate == alt_stubbed_receiver + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) + assert account_info.delegate == async_stubbed_receiver assert account_info.delegated_amount == expected_amount_delegated @pytest.mark.integration @pytest.mark.asyncio async def test_revoke( - alt_stubbed_sender, alt_stubbed_receiver, alt_stubbed_sender_token_account_pk, test_token, test_http_client_async + async_stubbed_sender, + async_stubbed_receiver, + async_stubbed_sender_token_account_pk, + test_token, + test_http_client_async, ): # pylint: disable=redefined-outer-name """Test revoke for undelgating a token account.""" expected_amount_delegated = 500 - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) - assert account_info.delegate == alt_stubbed_receiver + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) + assert account_info.delegate == async_stubbed_receiver assert account_info.delegated_amount == expected_amount_delegated revoke_resp = await test_token.revoke( - account=alt_stubbed_sender_token_account_pk, - owner=alt_stubbed_sender.public_key(), + account=async_stubbed_sender_token_account_pk, + owner=async_stubbed_sender.public_key(), ) revoke_confirmed = await aconfirm_transaction(test_http_client_async, revoke_resp["result"]) assert_valid_response(revoke_confirmed) - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) assert account_info.delegate is None assert account_info.delegated_amount == 0 @@ -346,85 +356,89 @@ async def test_revoke( @pytest.mark.integration @pytest.mark.asyncio async def test_approve_checked( - alt_stubbed_sender, alt_stubbed_receiver, alt_stubbed_sender_token_account_pk, test_token, test_http_client_async + async_stubbed_sender, + async_stubbed_receiver, + async_stubbed_sender_token_account_pk, + test_token, + test_http_client_async, ): # pylint: disable=redefined-outer-name """Test approve_checked for delegating a token account.""" expected_amount_delegated = 500 resp = await test_token.approve_checked( - source=alt_stubbed_sender_token_account_pk, - delegate=alt_stubbed_receiver, - owner=alt_stubbed_sender.public_key(), + source=async_stubbed_sender_token_account_pk, + delegate=async_stubbed_receiver, + owner=async_stubbed_sender.public_key(), amount=expected_amount_delegated, decimals=6, ) confirmed = await aconfirm_transaction(test_http_client_async, resp["result"]) assert_valid_response(confirmed) - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) - assert account_info.delegate == alt_stubbed_receiver + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) + assert account_info.delegate == async_stubbed_receiver assert account_info.delegated_amount == expected_amount_delegated @pytest.mark.integration @pytest.mark.asyncio async def test_freeze_account( - alt_stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async + async_stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async ): # pylint: disable=redefined-outer-name """Test freezing an account.""" resp = await test_http_client_async.request_airdrop(freeze_authority.public_key(), AIRDROP_AMOUNT) confirmed = await aconfirm_transaction(test_http_client_async, resp["result"]) assert_valid_response(confirmed) - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) assert account_info.is_frozen is False - freeze_resp = await test_token.freeze_account(alt_stubbed_sender_token_account_pk, freeze_authority) + freeze_resp = await test_token.freeze_account(async_stubbed_sender_token_account_pk, freeze_authority) freeze_confirmed = await aconfirm_transaction(test_http_client_async, freeze_resp["result"]) assert_valid_response(freeze_confirmed) - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) assert account_info.is_frozen is True @pytest.mark.integration @pytest.mark.asyncio async def test_thaw_account( - alt_stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async + async_stubbed_sender_token_account_pk, freeze_authority, test_token, test_http_client_async ): # pylint: disable=redefined-outer-name """Test thawing an account.""" - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) assert account_info.is_frozen is True - thaw_resp = await test_token.thaw_account(alt_stubbed_sender_token_account_pk, freeze_authority) + thaw_resp = await test_token.thaw_account(async_stubbed_sender_token_account_pk, freeze_authority) thaw_confirmed = await aconfirm_transaction(test_http_client_async, thaw_resp["result"]) assert_valid_response(thaw_confirmed) - account_info = await test_token.get_account_info(alt_stubbed_sender_token_account_pk) + account_info = await test_token.get_account_info(async_stubbed_sender_token_account_pk) assert account_info.is_frozen is False @pytest.mark.integration @pytest.mark.asyncio async def test_close_account( - alt_stubbed_sender, - alt_stubbed_sender_token_account_pk, - alt_stubbed_receiver_token_account_pk, + async_stubbed_sender, + async_stubbed_sender_token_account_pk, + async_stubbed_receiver_token_account_pk, test_token, test_http_client_async, ): # pylint: disable=redefined-outer-name """Test closing a token account.""" - create_resp = await test_http_client_async.get_account_info(alt_stubbed_sender_token_account_pk) + create_resp = await test_http_client_async.get_account_info(async_stubbed_sender_token_account_pk) assert_valid_response(create_resp) assert create_resp["result"]["value"]["data"] close_resp = await test_token.close_account( - account=alt_stubbed_sender_token_account_pk, - dest=alt_stubbed_receiver_token_account_pk, - authority=alt_stubbed_sender, + account=async_stubbed_sender_token_account_pk, + dest=async_stubbed_receiver_token_account_pk, + authority=async_stubbed_sender, ) close_confirmed = await aconfirm_transaction(test_http_client_async, close_resp["result"]) assert_valid_response(close_confirmed) - info_resp = await test_http_client_async.get_account_info(alt_stubbed_sender_token_account_pk) + info_resp = await test_http_client_async.get_account_info(async_stubbed_sender_token_account_pk) assert_valid_response(info_resp) assert info_resp["result"]["value"] is None @@ -432,12 +446,12 @@ async def test_close_account( @pytest.mark.integration @pytest.mark.asyncio async def test_create_multisig( - alt_stubbed_sender, alt_stubbed_receiver, test_token, test_http_client + async_stubbed_sender, async_stubbed_receiver, test_token, test_http_client ): # pylint: disable=redefined-outer-name """Test creating a multisig account.""" min_signers = 2 multisig_pubkey = await test_token.create_multisig( - min_signers, [alt_stubbed_sender.public_key(), alt_stubbed_receiver] + min_signers, [async_stubbed_sender.public_key(), async_stubbed_receiver] ) resp = test_http_client.get_account_info(multisig_pubkey) assert_valid_response(resp) @@ -446,5 +460,5 @@ async def test_create_multisig( multisig_data = layouts.MULTISIG_LAYOUT.parse(decode_byte_string(resp["result"]["value"]["data"][0])) assert multisig_data.is_initialized assert multisig_data.m == min_signers - assert PublicKey(multisig_data.signer1) == alt_stubbed_sender.public_key() - assert PublicKey(multisig_data.signer2) == alt_stubbed_receiver + assert PublicKey(multisig_data.signer1) == async_stubbed_sender.public_key() + assert PublicKey(multisig_data.signer2) == async_stubbed_receiver diff --git a/tests/integration/test_http_client.py b/tests/integration/test_http_client.py index 24441fbb..806425d9 100644 --- a/tests/integration/test_http_client.py +++ b/tests/integration/test_http_client.py @@ -19,6 +19,28 @@ def test_request_air_drop(stubbed_sender, test_http_client): assert resp["result"]["meta"] == expected_meta +@pytest.mark.integration +def test_request_air_drop_prefetched_blockhash(stubbed_sender_prefetched_blockhash, test_http_client): + """Test air drop to stubbed_sender.""" + resp = test_http_client.request_airdrop(stubbed_sender_prefetched_blockhash.public_key(), AIRDROP_AMOUNT) + assert_valid_response(resp) + resp = confirm_transaction(test_http_client, resp["result"]) + assert_valid_response(resp) + expected_meta = generate_expected_meta_after_airdrop(resp) + assert resp["result"]["meta"] == expected_meta + + +@pytest.mark.integration +def test_request_air_drop_cached_blockhash(stubbed_sender_cached_blockhash, test_http_client): + """Test air drop to stubbed_sender.""" + resp = test_http_client.request_airdrop(stubbed_sender_cached_blockhash.public_key(), AIRDROP_AMOUNT) + assert_valid_response(resp) + resp = confirm_transaction(test_http_client, resp["result"]) + assert_valid_response(resp) + expected_meta = generate_expected_meta_after_airdrop(resp) + assert resp["result"]["meta"] == expected_meta + + @pytest.mark.integration def test_send_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test_http_client): """Test sending a transaction to localnet.""" @@ -66,6 +88,154 @@ def test_send_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test assert resp["result"]["value"] == 954 +@pytest.mark.integration +def test_send_transaction_prefetched_blockhash( + stubbed_sender_prefetched_blockhash, stubbed_receiver_prefetched_blockhash, test_http_client +): + """Test sending a transaction to localnet.""" + # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction().add( + sp.transfer( + sp.TransferParams( + from_pubkey=stubbed_sender_prefetched_blockhash.public_key(), + to_pubkey=stubbed_receiver_prefetched_blockhash, + lamports=1000, + ) + ) + ) + recent_blockhash = test_http_client.parse_recent_blockhash(test_http_client.get_recent_blockhash()) + resp = test_http_client.send_transaction( + transfer_tx, stubbed_sender_prefetched_blockhash, recent_blockhash=recent_blockhash + ) + assert_valid_response(resp) + # Confirm transaction + resp = confirm_transaction(test_http_client, resp["result"]) + assert_valid_response(resp) + expected_meta = { + "err": None, + "fee": 5000, + "innerInstructions": [], + "logMessages": [ + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + ], + "postBalances": [9999994000, 954, 1], + "postTokenBalances": [], + "preBalances": [10000000000, 0, 1], + "preTokenBalances": [], + "rewards": [ + { + "commission": None, + "lamports": -46, + "postBalance": 954, + "pubkey": "J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i97", + "rewardType": "Rent", + } + ], + "status": {"Ok": None}, + } + assert resp["result"]["meta"] == expected_meta + # Check balances + resp = test_http_client.get_balance(stubbed_sender_prefetched_blockhash.public_key()) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999994000 + resp = test_http_client.get_balance(stubbed_receiver_prefetched_blockhash) + assert_valid_response(resp) + assert resp["result"]["value"] == 954 + + +@pytest.mark.integration +def test_send_transaction_cached_blockhash( + stubbed_sender_cached_blockhash, stubbed_receiver_cached_blockhash, test_http_client_cached_blockhash +): + """Test sending a transaction to localnet.""" + # Create transfer tx to transfer lamports from stubbed sender to stubbed_receiver + transfer_tx = Transaction().add( + sp.transfer( + sp.TransferParams( + from_pubkey=stubbed_sender_cached_blockhash.public_key(), + to_pubkey=stubbed_receiver_cached_blockhash, + lamports=1000, + ) + ) + ) + resp = test_http_client_cached_blockhash.send_transaction(transfer_tx, stubbed_sender_cached_blockhash) + assert_valid_response(resp) + # Confirm transaction + resp = confirm_transaction(test_http_client_cached_blockhash, resp["result"]) + assert_valid_response(resp) + expected_meta = { + "err": None, + "fee": 5000, + "innerInstructions": [], + "logMessages": [ + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + ], + "postBalances": [9999994000, 954, 1], + "postTokenBalances": [], + "preBalances": [10000000000, 0, 1], + "preTokenBalances": [], + "rewards": [ + { + "commission": None, + "lamports": -46, + "postBalance": 954, + "pubkey": "J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i95", + "rewardType": "Rent", + } + ], + "status": {"Ok": None}, + } + assert resp["result"]["meta"] == expected_meta + # Check balances + resp = test_http_client_cached_blockhash.get_balance(stubbed_sender_cached_blockhash.public_key()) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999994000 + assert len(test_http_client_cached_blockhash.blockhash_cache.unused_blockhashes) == 1 + + # Second transaction + transfer_tx = Transaction().add( + sp.transfer( + sp.TransferParams( + from_pubkey=stubbed_sender_cached_blockhash.public_key(), + to_pubkey=stubbed_receiver_cached_blockhash, + lamports=2000, + ) + ) + ) + resp = test_http_client_cached_blockhash.get_balance(stubbed_receiver_cached_blockhash) + assert_valid_response(resp) + assert resp["result"]["value"] == 954 + resp = test_http_client_cached_blockhash.send_transaction(transfer_tx, stubbed_sender_cached_blockhash) + assert_valid_response(resp) + # Confirm transaction + resp = confirm_transaction(test_http_client_cached_blockhash, resp["result"]) + assert_valid_response(resp) + expected_meta = { + "err": None, + "fee": 5000, + "innerInstructions": [], + "logMessages": [ + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success", + ], + "postBalances": [9999987000, 2954, 1], + "postTokenBalances": [], + "preBalances": [9999994000, 954, 1], + "preTokenBalances": [], + "rewards": [], + "status": {"Ok": None}, + } + assert resp["result"]["meta"] == expected_meta + # Check balances + resp = test_http_client_cached_blockhash.get_balance(stubbed_sender_cached_blockhash.public_key()) + assert_valid_response(resp) + assert resp["result"]["value"] == 9999987000 + assert len(test_http_client_cached_blockhash.blockhash_cache.unused_blockhashes) == 1 + assert len(test_http_client_cached_blockhash.blockhash_cache.used_blockhashes) == 1 + + @pytest.mark.integration def test_send_raw_transaction_and_get_balance(stubbed_sender, stubbed_receiver, test_http_client): """Test sending a raw transaction to localnet."""