From 99540721bad1382efda40343b4fb4c29f465f6a6 Mon Sep 17 00:00:00 2001 From: Leonid Tyurin Date: Mon, 7 Nov 2022 23:22:49 +0400 Subject: [PATCH 1/4] Reworked approach to resend transactions (#96) --- CONFIGURATION.md | 3 + yarn.lock | 457 ++++++++++-------- zp-relayer/config.ts | 6 +- zp-relayer/init.ts | 6 +- zp-relayer/package.json | 12 +- zp-relayer/queue/poolTxQueue.ts | 7 +- zp-relayer/queue/sentTxQueue.ts | 21 +- zp-relayer/services/gas-price/GasPrice.ts | 114 ++++- zp-relayer/state/rootSet.ts | 2 +- zp-relayer/test/depositMemo.json | 1 - zp-relayer/test/pool.test.ts | 35 -- zp-relayer/test/unit-tests/GasPrice.test.ts | 61 +++ zp-relayer/test/unit-tests/validateTx.test.ts | 13 + zp-relayer/tx/signAndSend.ts | 9 +- zp-relayer/utils/constants.ts | 1 - zp-relayer/utils/helpers.ts | 34 +- zp-relayer/utils/redisFields.ts | 6 - zp-relayer/validateTx.ts | 2 - zp-relayer/workers/poolTxWorker.ts | 78 +-- zp-relayer/workers/sentTxWorker.ts | 239 ++++----- 20 files changed, 650 insertions(+), 457 deletions(-) delete mode 100644 zp-relayer/test/depositMemo.json delete mode 100644 zp-relayer/test/pool.test.ts create mode 100644 zp-relayer/test/unit-tests/GasPrice.test.ts create mode 100644 zp-relayer/test/unit-tests/validateTx.test.ts diff --git a/CONFIGURATION.md b/CONFIGURATION.md index ade5c266..22fee203 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -19,7 +19,10 @@ | GAS_PRICE_SPEED_TYPE | This parameter specifies the desirable transaction speed | `instant` / `fast` / `standard` / `low` | | GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer | | GAS_PRICE_UPDATE_INTERVAL | Interval in milliseconds used to get the updated gas price value using specified estimation type | integer | +| GAS_PRICE_SURPLUS | A surplus to be added to fetched `gasPrice` on initial transaction submission. Default `0.1`. | float | +| MIN_GAS_PRICE_BUMP_FACTOR | Minimum `gasPrice` bump factor to meet RPC node requirements. Default `0.1`. | float | | MAX_FEE_PER_GAS_LIMIT | Max limit on `maxFeePerGas` parameter for each transaction in wei | integer | +| MAX_SENT_QUEUE_SIZE | Maximum number of jobs waiting in the `sentTxQueue` at a time. | integer | | START_BLOCK | The block number used to start searching for events when the relayer instance is run for the first time | integer | EVENTS_PROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs. Defaults to `10000` | integer | RELAYER_LOG_LEVEL | Log level | Winston log level | diff --git a/yarn.lock b/yarn.lock index ef602618..0c9e8716 100644 --- a/yarn.lock +++ b/yarn.lock @@ -45,9 +45,9 @@ kuler "^2.0.0" "@discoveryjs/json-ext@^0.5.0": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3" - integrity sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA== + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== "@ethereumjs/common@^2.5.0", "@ethereumjs/common@^2.6.4": version "2.6.5" @@ -246,6 +246,11 @@ resolved "https://registry.yarnpkg.com/@findeth/abi/-/abi-0.7.1.tgz#60d0801cb252e587dc3228f00c00581bb748aebc" integrity sha512-9uNu+/UxeuIibxIB7slf7BGG2PWjgBZr+rKzohhLb7VuoZjmlCcKZkenqwErROxkPdsap7OGO/o1DuYMvObMvw== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@jest/types@^27.2.5": version "27.2.5" resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.2.5.tgz#420765c052605e75686982d24b061b4cbba22132" @@ -268,35 +273,35 @@ tweetnacl "^1.0.3" tweetnacl-util "^0.15.1" -"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.0.2.tgz#01e3669b8b2dc01f6353f2c87e1ec94faf52c587" - integrity sha512-FMX5i7a+ojIguHpWbzh5MCsCouJkwf4z4ejdUY/fsgB9Vkdak4ZnoIEskOyOUMMB4lctiZFGszFQJXUeFL8tRg== +"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz#9571b87be3a3f2c46de05585470bc4f3af2f6f00" + integrity sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ== -"@msgpackr-extract/msgpackr-extract-darwin-x64@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.0.2.tgz#5ca32f16e6f1b7854001a1a2345b61d4e26a0931" - integrity sha512-DznYtF3lHuZDSRaIOYeif4JgO0NtO2Xf8DsngAugMx/bUdTFbg86jDTmkVJBNmV+cxszz6OjGvinnS8AbJ342g== +"@msgpackr-extract/msgpackr-extract-darwin-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.1.2.tgz#bfbc6936ede2955218f5621a675679a5fe8e6f4c" + integrity sha512-YPXtcVkhmVNoMGlqp81ZHW4dMxK09msWgnxtsDpSiZwTzUBG2N+No2bsr7WMtBKCVJMSD6mbAl7YhKUqkp/Few== -"@msgpackr-extract/msgpackr-extract-linux-arm64@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.0.2.tgz#ff629f94379981bf476dffb1439a7c1d3dba2d72" - integrity sha512-b0jMEo566YdM2K+BurSed7bswjo3a6bcdw5ETqoIfSuxKuRLPfAiOjVbZyZBgx3J/TAM/QrvEQ/VN89A0ZAxSg== +"@msgpackr-extract/msgpackr-extract-linux-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.1.2.tgz#22555e28382af2922e7450634c8a2f240bb9eb82" + integrity sha512-vHZ2JiOWF2+DN9lzltGbhtQNzDo8fKFGrf37UJrgqxU0yvtERrzUugnfnX1wmVfFhSsF8OxrfqiNOUc5hko1Zg== -"@msgpackr-extract/msgpackr-extract-linux-arm@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.0.2.tgz#5f6fd30d266c4a90cf989049c7f2e50e5d4fcd4c" - integrity sha512-Gy9+c3Wj+rUlD3YvCZTi92gs+cRX7ZQogtwq0IhRenloTTlsbpezNgk6OCkt59V4ATEWSic9rbU92H/l7XsRvA== +"@msgpackr-extract/msgpackr-extract-linux-arm@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.1.2.tgz#ffb6ae1beea7ac572b6be6bf2a8e8162ebdd8be7" + integrity sha512-42R4MAFeIeNn+L98qwxAt360bwzX2Kf0ZQkBBucJ2Ircza3asoY4CDbgiu9VWklq8gWJVSJSJBwDI+c/THiWkA== -"@msgpackr-extract/msgpackr-extract-linux-x64@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.0.2.tgz#167faa553b9dbffac8b03bf27de9b6f846f0e1bc" - integrity sha512-zrBHaePwcv4cQXxzYgNj0+A8I1uVN97E7/3LmkRocYZ+rMwUsnPpp4RuTAHSRoKlTQV3nSdCQW4Qdt4MXw/iHw== +"@msgpackr-extract/msgpackr-extract-linux-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.1.2.tgz#7caf62eebbfb1345de40f75e89666b3d4194755f" + integrity sha512-RjRoRxg7Q3kPAdUSC5EUUPlwfMkIVhmaRTIe+cqHbKrGZ4M6TyCA/b5qMaukQ/1CHWrqYY2FbKOAU8Hg0pQFzg== -"@msgpackr-extract/msgpackr-extract-win32-x64@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.0.2.tgz#baea7764b1adf201ce4a792fe971fd7211dad2e4" - integrity sha512-fpnI00dt+yO1cKx9qBXelKhPBdEgvc8ZPav1+0r09j0woYQU2N79w/jcGawSY5UGlgQ3vjaJsFHnGbGvvqdLzg== +"@msgpackr-extract/msgpackr-extract-win32-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz#f2d8b9ddd8d191205ed26ce54aba3dfc5ae3e7c9" + integrity sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw== "@mycrypto/eth-scan@3.5.2", "@mycrypto/eth-scan@3.5.3": version "3.5.3" @@ -389,6 +394,18 @@ "@types/connect" "*" "@types/node" "*" +"@types/chai-as-promised@^7.1.5": + version "7.1.5" + resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz#6e016811f6c7a64f2eed823191c3a6955094e255" + integrity sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ== + dependencies: + "@types/chai" "*" + +"@types/chai@*": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.3.tgz#3c90752792660c4b562ad73b3fbd68bf3bc7ae07" + integrity sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g== + "@types/chai@^4.2.21": version "4.2.22" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.22.tgz#47020d7e4cf19194d43b5202f35f75bd2ad35ce7" @@ -583,11 +600,6 @@ dependencies: "@types/yargs-parser" "*" -"@ungap/promise-all-settled@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" - integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== - "@webassemblyjs/ast@1.11.1": version "1.11.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" @@ -1002,6 +1014,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bignumber.js@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.0.tgz#8d340146107fe3a6cb8d40699643c302e8773b62" + integrity sha512-4LwHK4nfDOraBCtst+wOWIHbu1vhvAPJK8g8nROd4iuc3PSEjWif/qwbkh8jwCJz6yDBvtU4KPynETgrfh7y3A== + bignumber.js@^9.0.0: version "9.0.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.0.1.tgz#8d7ba124c882bfd8e43260c67475518d0689e4e5" @@ -1089,6 +1106,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -1237,20 +1261,19 @@ builtin-status-codes@^3.0.0: resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= -bullmq@1.83.0: - version "1.83.0" - resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.83.0.tgz#925edd519374435e30a57467327f8f88095d4cce" - integrity sha512-KoA4xTgJyvwV8RschWlUmgXaNcylQzQw1u/XZvBF+vUa4xdPWc8xkM1hJ2hYIQVCjNx+Ofo1vysbNQw6K9x7rg== +bullmq@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-3.0.0.tgz#b660f1a926f7997014add8af95015668533a74b2" + integrity sha512-amw+YZhEo1B47iMpaLbtKwlzZjQi5NYjLCYl8n9qkQpkDDVAVJ9d++zdOgyXX6kG7i/pMP9tr2vyj3J6IcjbTA== dependencies: - cron-parser "^4.2.1" - get-port "^5.1.1" - glob "^7.2.0" - ioredis "^4.28.5" + cron-parser "^4.6.0" + glob "^8.0.3" + ioredis "^5.2.2" lodash "^4.17.21" - msgpackr "^1.4.6" + msgpackr "^1.6.2" semver "^7.3.7" - tslib "^1.14.1" - uuid "^8.3.2" + tslib "^2.0.0" + uuid "^9.0.0" bytes@3.1.0: version "3.1.0" @@ -1317,6 +1340,13 @@ caseless@~0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= +chai-as-promised@7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0" + integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA== + dependencies: + check-error "^1.0.2" + chai-bn@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/chai-bn/-/chai-bn-0.3.0.tgz#6310314dbb49590a8ec50b3fe12b6670141c120e" @@ -1356,10 +1386,10 @@ check-error@^1.0.2: resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= -chokidar@3.5.2, chokidar@^3.5.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" - integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== +chokidar@3.5.3, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -1371,10 +1401,10 @@ chokidar@3.5.2, chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" -chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +chokidar@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75" + integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -1539,9 +1569,9 @@ color@^3.1.3: color-string "^1.6.0" colorette@^2.0.14: - version "2.0.16" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da" - integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g== + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== colors@^1.2.1, colors@^1.4.0: version "1.4.0" @@ -1724,12 +1754,12 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cron-parser@^4.2.1: - version "4.4.0" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.4.0.tgz#829d67f9e68eb52fa051e62de0418909f05db983" - integrity sha512-TrE5Un4rtJaKgmzPewh67yrER5uKM0qI9hGLDBfWb8GGRe9pn/SDkhVrdHa4z7h0SeyeNxnQnogws/H+AQANQA== +cron-parser@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-4.6.0.tgz#404c3fdbff10ae80eef6b709555d577ef2fd2e0d" + integrity sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA== dependencies: - luxon "^1.28.0" + luxon "^3.0.1" cross-spawn@^6.0.5: version "6.0.5" @@ -1814,10 +1844,10 @@ debug@3.2.6: dependencies: ms "^2.1.1" -debug@4.3.2, debug@^4.1.1, debug@^4.3.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== +debug@4.3.4, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" @@ -1828,6 +1858,13 @@ debug@^3.1.1, debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.1.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + decamelize-keys@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" @@ -1894,10 +1931,10 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -denque@^1.1.0: - version "1.5.1" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" - integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== +denque@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== depd@~1.1.2: version "1.1.2" @@ -1953,6 +1990,13 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +docker-compose@0.23.17: + version "0.23.17" + resolved "https://registry.yarnpkg.com/docker-compose/-/docker-compose-0.23.17.tgz#8816bef82562d9417dc8c790aa4871350f93a2ba" + integrity sha512-YJV18YoYIcxOdJKeFcCFihE6F4M2NExWM/d4S1ITcS9samHKnNUihz9kjggr0dNtsrbpFNc7/Yzd19DWs+m1xg== + dependencies: + yaml "^1.10.2" + dom-walk@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/dom-walk/-/dom-walk-0.1.2.tgz#0c548bef048f4d1f2a97249002236060daa3fd84" @@ -2442,9 +2486,9 @@ fast-json-stable-stringify@^2.0.0: integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fastest-levenshtein@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz#9990f7d3a88cc5a9ffd1f1745745251700d497e2" - integrity sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow== + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== fastq@^1.6.0: version "1.13.0" @@ -2637,11 +2681,6 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.1" -get-port@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" - integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== - get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -2705,19 +2744,7 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@7.1.7: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.1.3: +glob@7.2.0, glob@^7.1.3: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -2729,17 +2756,16 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.2.0: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== +glob@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.0.3.tgz#415c6eb2deed9e502c68fa44a272e6da6eeca42e" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" inherits "2" - minimatch "^3.1.1" + minimatch "^5.0.1" once "^1.3.0" - path-is-absolute "^1.0.0" global-dirs@^3.0.0: version "3.0.0" @@ -3036,9 +3062,9 @@ import-local@^2.0.0: resolve-cwd "^2.0.0" import-local@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.0.3.tgz#4d51c2c495ca9393da259ec66b62e022920211e0" - integrity sha512-bE9iaUY3CXH8Cwfan/abDKAxe1KGT9kyGsBPqf6DMK/z0a2OzAsrukeYNgIH6cH5Xr452jb1TUL8rSfCLjZ9uA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== dependencies: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" @@ -3095,36 +3121,17 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -ioredis@4.27.10: - version "4.27.10" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.27.10.tgz#3da6c1d2eab440f94c52d6fcd9b91127d7e07470" - integrity sha512-BtV2mEoZlhnW0EyxuK49V5iutLeZeJAYi/+Fuc4Q6DpDjq0cGMLODdS/+Kb5CHpT7v3YT6SK0vgJF6y0Ls4+Bg== - dependencies: - cluster-key-slot "^1.1.0" - debug "^4.3.1" - denque "^1.1.0" - lodash.defaults "^4.2.0" - lodash.flatten "^4.4.0" - lodash.isarguments "^3.1.0" - p-map "^2.1.0" - redis-commands "1.7.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.1.0" - -ioredis@^4.28.5: - version "4.28.5" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-4.28.5.tgz#5c149e6a8d76a7f8fa8a504ffc85b7d5b6797f9f" - integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== +ioredis@5.2.4, ioredis@^5.2.2: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.2.4.tgz#9e262a668bc29bae98f2054c1e0d7efd86996b96" + integrity sha512-qIpuAEt32lZJQ0XyrloCRdlEdUUNGG9i0UOk6zgzK6igyudNWqEBxfH6OlbnOOoBBvr1WB02mm8fR55CnikRng== dependencies: + "@ioredis/commands" "^1.1.1" cluster-key-slot "^1.1.0" - debug "^4.3.1" - denque "^1.1.0" + debug "^4.3.4" + denque "^2.0.1" lodash.defaults "^4.2.0" - lodash.flatten "^4.4.0" lodash.isarguments "^3.1.0" - p-map "^2.1.0" - redis-commands "1.7.0" redis-errors "^1.2.0" redis-parser "^3.0.0" standard-as-callback "^2.1.0" @@ -3198,6 +3205,13 @@ is-core-module@^2.2.0: dependencies: has "^1.0.3" +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + is-date-object@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -3419,7 +3433,7 @@ isexe@^2.0.0: isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" - integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== isomorphic-unfetch@^3.1.0: version "3.1.0" @@ -3698,11 +3712,6 @@ lodash.defaults@^4.2.0: resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= - lodash.isarguments@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" @@ -3769,10 +3778,10 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -luxon@^1.28.0: - version "1.28.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf" - integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== +luxon@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.1.0.tgz#9ac33d7142b7ea18d4ec8583cdeb0b079abef60d" + integrity sha512-7w6hmKC0/aoWnEsmPCu5Br54BmbmUp5GfcqBxQngRcXJ+q5fdfjEzn7dxmJh2YdDhgW8PccYtlWKSv4tQkrTQg== make-dir@^3.0.0: version "3.1.0" @@ -3922,12 +3931,19 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" -minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== +minimatch@5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b" + integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g== dependencies: - brace-expansion "^1.1.7" + brace-expansion "^2.0.1" + +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" minimist-options@^3.0.1: version "3.0.2" @@ -4009,6 +4025,33 @@ mocha-chrome@^2.2.0: meow "^5.0.0" nanobus "^4.2.0" +mocha@10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a" + integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg== + dependencies: + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.4" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "5.0.1" + ms "2.1.3" + nanoid "3.3.3" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + workerpool "6.2.1" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + mocha@6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.2.tgz#5d8987e28940caf8957a7d7664b910dc5b2fea20" @@ -4038,36 +4081,6 @@ mocha@6.2.2: yargs-parser "13.1.1" yargs-unparser "1.6.0" -mocha@^9.0.3: - version "9.1.2" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.2.tgz#93f53175b0f0dc4014bd2d612218fccfcf3534d3" - integrity sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w== - dependencies: - "@ungap/promise-all-settled" "1.1.2" - ansi-colors "4.1.1" - browser-stdout "1.3.1" - chokidar "3.5.2" - debug "4.3.2" - diff "5.0.0" - escape-string-regexp "4.0.0" - find-up "5.0.0" - glob "7.1.7" - growl "1.10.5" - he "1.2.0" - js-yaml "4.1.0" - log-symbols "4.1.0" - minimatch "3.0.4" - ms "2.1.3" - nanoid "3.1.25" - serialize-javascript "6.0.0" - strip-json-comments "3.1.1" - supports-color "8.1.1" - which "2.0.2" - workerpool "6.1.5" - yargs "16.2.0" - yargs-parser "20.2.4" - yargs-unparser "2.0.0" - mock-fs@^4.1.0: version "4.14.0" resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.14.0.tgz#ce5124d2c601421255985e6e94da80a7357b1b18" @@ -4093,26 +4106,26 @@ ms@2.1.3, ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -msgpackr-extract@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.0.2.tgz#201a8d7ade47e99b3ba277c45736b00e195d4670" - integrity sha512-coskCeJG2KDny23zWeu+6tNy7BLnAiOGgiwzlgdm4oeSsTpqEJJPguHIuKZcCdB7tzhZbXNYSg6jZAXkZErkJA== +msgpackr-extract@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-2.1.2.tgz#56272030f3e163e1b51964ef8b1cd5e7240c03ed" + integrity sha512-cmrmERQFb19NX2JABOGtrKdHMyI6RUyceaPBQ2iRz9GnDkjBWFjNJC0jyyoOfZl2U/LZE3tQCCQc4dlRyA8mcA== dependencies: - node-gyp-build-optional-packages "5.0.2" + node-gyp-build-optional-packages "5.0.3" optionalDependencies: - "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.0.2" - "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.0.2" - "@msgpackr-extract/msgpackr-extract-linux-arm" "2.0.2" - "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.0.2" - "@msgpackr-extract/msgpackr-extract-linux-x64" "2.0.2" - "@msgpackr-extract/msgpackr-extract-win32-x64" "2.0.2" - -msgpackr@^1.4.6: - version "1.6.0" - resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.6.0.tgz#faa80298cbc7fd949175d73674c31c3ab3c0172c" - integrity sha512-CJs2OuaIuwpP2iLZx6vl/jfl7WqFNFrYpkp/BC1ctzCbYAACyT9lYMACstgvH4pTcBrCFk4uzOoOZj0gFP/0EA== + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-win32-x64" "2.1.2" + +msgpackr@^1.6.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/msgpackr/-/msgpackr-1.7.2.tgz#68d6debf5999d6b61abb6e7046a689991ebf7261" + integrity sha512-mWScyHTtG6TjivXX9vfIy2nBtRupaiAj0HQ2mtmpmYujAmqZmaaEVPaSZ1NKLMvicaMLFzEaMk0ManxMRg8rMQ== optionalDependencies: - msgpackr-extract "^2.0.2" + msgpackr-extract "^2.1.2" multibase@^0.7.0: version "0.7.0" @@ -4183,10 +4196,10 @@ nanocolors@^0.2.12: resolved "https://registry.yarnpkg.com/nanocolors/-/nanocolors-0.2.12.tgz#4d05932e70116078673ea4cc6699a1c56cc77777" integrity sha512-SFNdALvzW+rVlzqexid6epYdt8H9Zol7xDoQarioEFcFN0JHo4CYNztAxmtfgGTVRCmFlEOqqhBpoFGKqSAMug== -nanoid@3.1.25: - version "3.1.25" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152" - integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q== +nanoid@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25" + integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w== nanoid@^3.1.28: version "3.3.4" @@ -4255,10 +4268,10 @@ node-fetch@^2.6.1: dependencies: whatwg-url "^5.0.0" -node-gyp-build-optional-packages@5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.2.tgz#3de7d30bd1f9057b5dfbaeab4a4442b7fe9c5901" - integrity sha512-PiN4NWmlQPqvbEFcH/omQsswWQbe5Z9YK/zdB23irp5j2XibaA2IrGvpSWmVVG4qMZdmPdwPctSy4a86rOMn6g== +node-gyp-build-optional-packages@5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz#92a89d400352c44ad3975010368072b41ad66c17" + integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== node-gyp-build@^4.2.0: version "4.3.0" @@ -4539,11 +4552,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" - integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== - p-timeout@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-1.2.1.tgz#5eb3b353b7fce99f101a1038880bb054ebbea386" @@ -4635,7 +4643,7 @@ path-key@^3.0.0, path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== @@ -4983,11 +4991,6 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" -redis-commands@1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" - integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== - redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" @@ -5089,7 +5092,7 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== -resolve@^1.10.0, resolve@^1.9.0: +resolve@^1.10.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -5097,6 +5100,15 @@ resolve@^1.10.0, resolve@^1.9.0: is-core-module "^2.2.0" path-parse "^1.0.6" +resolve@^1.9.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + responselike@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -5635,6 +5647,11 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + swarm-js@^0.1.40: version "0.1.40" resolved "https://registry.yarnpkg.com/swarm-js/-/swarm-js-0.1.40.tgz#b1bc7b6dcc76061f6c772203e004c11997e06b99" @@ -5847,11 +5864,16 @@ tsconfig-paths@^4.1.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.14.1, tslib@^1.9.0: +tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.0.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + tslib@^2.3.1: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" @@ -6081,10 +6103,10 @@ uuid@^3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== validate-npm-package-license@^3.0.1: version "3.0.4" @@ -6475,7 +6497,7 @@ which@1.3.1, which@^1.2.9: dependencies: isexe "^2.0.0" -which@2.0.2, which@^2.0.1: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -6524,10 +6546,10 @@ winston@3.3.3: triple-beam "^1.3.0" winston-transport "^4.4.0" -workerpool@6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581" - integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw== +workerpool@6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" + integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== wrap-ansi@^5.1.0: version "5.1.0" @@ -6648,6 +6670,11 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + yargs-parser@13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" diff --git a/zp-relayer/config.ts b/zp-relayer/config.ts index 27554bc9..10e7c4b2 100644 --- a/zp-relayer/config.ts +++ b/zp-relayer/config.ts @@ -25,15 +25,17 @@ const config = { gasPriceSpeedType: (process.env.GAS_PRICE_SPEED_TYPE as GasPriceKey) || 'fast', gasPriceFactor: parseInt(process.env.GAS_PRICE_FACTOR || '1'), gasPriceUpdateInterval: parseInt(process.env.GAS_PRICE_UPDATE_INTERVAL || '5000'), + gasPriceSurplus: parseFloat(process.env.GAS_PRICE_SURPLUS || '0.1'), + minGasPriceBumpFactor: parseFloat(process.env.MIN_GAS_PRICE_BUMP_FACTOR || '0.1'), maxFeeLimit: process.env.MAX_FEE_PER_GAS_LIMIT ? toBN(process.env.MAX_FEE_PER_GAS_LIMIT) : null, + maxSentQueueSize: parseInt(process.env.MAX_SENT_QUEUE_SIZE || '20'), startBlock: parseInt(process.env.START_BLOCK || '0'), eventsProcessingBatchSize: parseInt(process.env.EVENTS_PROCESSING_BATCH_SIZE || '10000'), logLevel: process.env.RELAYER_LOG_LEVEL || 'debug', - redisUrl: process.env.RELAYER_REDIS_URL, + redisUrl: process.env.RELAYER_REDIS_URL as string, rpcUrl: process.env.RPC_URL as string, sentTxDelay: parseInt(process.env.SENT_TX_DELAY || '30000'), permitDeadlineThresholdInitial: parseInt(process.env.PERMIT_DEADLINE_THRESHOLD_INITIAL || '300'), - permitDeadlineThresholdResend: parseInt(process.env.PERMIT_DEADLINE_THRESHOLD_RESEND || '10'), } export default config diff --git a/zp-relayer/init.ts b/zp-relayer/init.ts index f5a35cb1..351849f2 100644 --- a/zp-relayer/init.ts +++ b/zp-relayer/init.ts @@ -7,6 +7,8 @@ import { Mutex } from 'async-mutex' import { createPoolTxWorker } from './workers/poolTxWorker' import { createSentTxWorker } from './workers/sentTxWorker' import { initializeDomain } from './utils/EIP712SaltedPermit' +import { redis } from './services/redisClient' +import { validateTx } from './validateTx' export async function init() { await initializeDomain(web3) @@ -19,6 +21,6 @@ export async function init() { }) await gasPriceService.start() const workerMutex = new Mutex() - ;(await createPoolTxWorker(gasPriceService, workerMutex)).run() - ;(await createSentTxWorker(gasPriceService, workerMutex)).run() + ;(await createPoolTxWorker(gasPriceService, validateTx, workerMutex, redis)).run() + ;(await createSentTxWorker(gasPriceService, workerMutex, redis)).run() } diff --git a/zp-relayer/package.json b/zp-relayer/package.json index c12c1a81..6eeb3e79 100644 --- a/zp-relayer/package.json +++ b/zp-relayer/package.json @@ -10,20 +10,21 @@ "dev:worker": "ts-node poolTxWorker.ts", "start:dev": "ts-node index.ts", "start:prod": "node index.js", - "test": "ts-mocha --paths --timeout 1000000 test/**/*.test.ts" + "test:unit": "ts-mocha -r dotenv/config --paths --timeout 1000000 test/unit-tests/*.test.ts" }, "dependencies": { "@metamask/eth-sig-util": "^4.0.1", "@mycrypto/gas-estimation": "^1.1.0", "ajv": "8.11.0", "async-mutex": "^0.3.2", - "bullmq": "1.83.0", + "bignumber.js": "9.1.0", + "bullmq": "3.0.0", "cors": "^2.8.5", "dotenv": "^10.0.0", "express": "^4.17.1", "express-winston": "4.2.0", "gas-price-oracle": "0.5.1", - "ioredis": "4.27.10", + "ioredis": "5.2.4", "libzkbob-rs-node": "0.1.27", "node-fetch": "^2.6.1", "promise-retry": "^2.0.1", @@ -34,6 +35,7 @@ }, "devDependencies": { "@types/chai": "^4.2.21", + "@types/chai-as-promised": "^7.1.5", "@types/cors": "^2.8.12", "@types/expect": "^24.3.0", "@types/express": "^4.17.13", @@ -43,8 +45,10 @@ "@types/node-fetch": "^2.5.12", "@types/promise-retry": "^1.1.3", "chai": "^4.3.4", + "chai-as-promised": "7.1.1", "concurrently": "7.1.0", - "mocha": "^9.0.3", + "docker-compose": "0.23.17", + "mocha": "10.1.0", "nodemon": "^2.0.12", "npm-run-all": "^4.1.5", "ts-mocha": "^8.0.0", diff --git a/zp-relayer/queue/poolTxQueue.ts b/zp-relayer/queue/poolTxQueue.ts index 86bfd915..c8c293e2 100644 --- a/zp-relayer/queue/poolTxQueue.ts +++ b/zp-relayer/queue/poolTxQueue.ts @@ -1,7 +1,7 @@ import { Queue } from 'bullmq' import { redis } from '@/services/redisClient' import { TX_QUEUE_NAME } from '@/utils/constants' -import { Proof } from 'libzkbob-rs-node' +import type { Proof } from 'libzkbob-rs-node' import { TxType } from 'zp-memo-parser' export interface TxPayload { @@ -12,6 +12,9 @@ export interface TxPayload { rawMemo: string depositSignature: string | null } -export const poolTxQueue = new Queue(TX_QUEUE_NAME, { + +export type PoolTxResult = [string, string] + +export const poolTxQueue = new Queue(TX_QUEUE_NAME, { connection: redis, }) diff --git a/zp-relayer/queue/sentTxQueue.ts b/zp-relayer/queue/sentTxQueue.ts index df3fdecd..1436db4e 100644 --- a/zp-relayer/queue/sentTxQueue.ts +++ b/zp-relayer/queue/sentTxQueue.ts @@ -1,36 +1,29 @@ -import { Queue, QueueScheduler } from 'bullmq' +import { Queue } from 'bullmq' import { redis } from '@/services/redisClient' import { SENT_TX_QUEUE_NAME } from '@/utils/constants' import type { TransactionConfig } from 'web3-core' import { GasPriceValue } from '@/services/gas-price' -import { TxData, TxType } from 'zp-memo-parser' +import { TxPayload } from './poolTxQueue' +export type SendAttempt = [string, GasPriceValue] export interface SentTxPayload { - txType: TxType root: string outCommit: string commitIndex: number - txHash: string prefixedMemo: string txConfig: TransactionConfig nullifier: string - gasPriceOptions: GasPriceValue - txData: TxData + txPayload: TxPayload + prevAttempts: SendAttempt[] } export enum SentTxState { MINED = 'MINED', REVERT = 'REVERT', - RESEND = 'RESEND', - FAILED = 'FAILED', + SKIPPED = 'SKIPPED', } -export type SentTxResult = [SentTxState, string] - -// Required for delayed jobs processing -const sentTxQueueScheduler = new QueueScheduler(SENT_TX_QUEUE_NAME, { - connection: redis, -}) +export type SentTxResult = [SentTxState, string, string[]] export const sentTxQueue = new Queue(SENT_TX_QUEUE_NAME, { connection: redis, diff --git a/zp-relayer/services/gas-price/GasPrice.ts b/zp-relayer/services/gas-price/GasPrice.ts index 509c4421..b4ee2959 100644 --- a/zp-relayer/services/gas-price/GasPrice.ts +++ b/zp-relayer/services/gas-price/GasPrice.ts @@ -1,6 +1,7 @@ import BN from 'bn.js' import type Web3 from 'web3' import { toWei, toBN } from 'web3-utils' +import BigNumber from 'bignumber.js' import config from '@/config' import { setIntervalAndRun } from '@/utils/helpers' import { estimateFees } from '@mycrypto/gas-estimation' @@ -50,15 +51,15 @@ export function chooseGasPriceOptions(a: GasPriceValue, b: GasPriceValue): GasPr return b } -export function EIP1559GasPriceWithinLimit(fees: EIP1559GasPrice, maxFeeLimit: BN | null): EIP1559GasPrice { - if (!maxFeeLimit) return fees +export function EIP1559GasPriceWithinLimit(gp: EIP1559GasPrice, maxFeeLimit: BN): EIP1559GasPrice { + if (!maxFeeLimit) return gp - const diff = toBN(fees.maxFeePerGas).sub(maxFeeLimit) + const diff = toBN(gp.maxFeePerGas).sub(maxFeeLimit) if (diff.isNeg()) { - return fees + return gp } else { const maxFeePerGas = maxFeeLimit.toString(10) - const maxPriorityFeePerGas = BN.min(toBN(fees.maxPriorityFeePerGas), maxFeeLimit).toString(10) + const maxPriorityFeePerGas = BN.min(toBN(gp.maxPriorityFeePerGas), maxFeeLimit).toString(10) return { maxFeePerGas, maxPriorityFeePerGas, @@ -66,6 +67,60 @@ export function EIP1559GasPriceWithinLimit(fees: EIP1559GasPrice, maxFeeLimit: B } } +export function LegacyGasPriceWithinLimit(gp: LegacyGasPrice, maxFeeLimit: BN): LegacyGasPrice { + if (!maxFeeLimit) return gp + + return { + gasPrice: BN.min(toBN(gp.gasPrice), maxFeeLimit).toString(10), + } +} + +export function gasPriceWithinLimit(gp: GasPriceValue, maxFeeLimit: BN | null): GasPriceValue { + if (!maxFeeLimit) return gp + if (isEIP1559GasPrice(gp)) { + return EIP1559GasPriceWithinLimit(gp, maxFeeLimit) + } + if (isLegacyGasPrice(gp)) { + return LegacyGasPriceWithinLimit(gp, maxFeeLimit) + } + return gp +} + +function addExtraGas(gas: BN, extraPercentage: number, maxGasLimit: string | undefined): BN { + const factor = BigNumber(1 + extraPercentage) + + const gasWithExtra = BigNumber(gas.toString(10)).multipliedBy(factor).toFixed(0) + + if (maxGasLimit) { + return toBN(BigNumber.min(maxGasLimit, gasWithExtra).toString(10)) + } else { + return toBN(gasWithExtra) + } +} + +export function addExtraGasPrice( + gp: GasPriceValue, + factor = config.minGasPriceBumpFactor, + maxFeeLimit: BN | null = config.maxFeeLimit +): GasPriceValue { + if (factor === 0) return gp + + const maxGasPrice = maxFeeLimit?.toString() + + if (isLegacyGasPrice(gp)) { + return { + gasPrice: addExtraGas(toBN(gp.gasPrice), factor, maxGasPrice).toString(), + } + } + if (isEIP1559GasPrice(gp)) { + return { + maxFeePerGas: addExtraGas(toBN(gp.maxFeePerGas), factor, maxGasPrice).toString(), + maxPriorityFeePerGas: addExtraGas(toBN(gp.maxPriorityFeePerGas), factor, maxGasPrice).toString(), + } + } + return gp +} + export class GasPrice { private fetchGasPriceInterval: NodeJS.Timeout | null = null private cachedGasPrice: GasPriceValue @@ -88,16 +143,30 @@ export class GasPrice { if (this.fetchGasPriceInterval) clearInterval(this.fetchGasPriceInterval) this.fetchGasPriceInterval = await setIntervalAndRun(async () => { - try { - this.cachedGasPrice = await this.fetchGasPrice(this.options) - logger.info('Updated gasPrice: %o', this.cachedGasPrice) - } catch (e) { - logger.warn('Failed to fetch gasPrice %o; using default value', e) - this.cachedGasPrice = GasPrice.defaultGasPrice - } + this.cachedGasPrice = await this.fetchOnce() }, this.updateInterval) } + async fetchOnce() { + let gasPrice + try { + gasPrice = await this.fetchGasPrice(this.options) + } catch (e) { + logger.warn('Failed to fetch gasPrice %s; using previous value', (e as Error).message) + gasPrice = chooseGasPriceOptions(GasPrice.defaultGasPrice, this.cachedGasPrice) + } + logger.info('Updated gasPrice: %o', gasPrice) + return gasPrice + } + + stop() { + if (this.fetchGasPriceInterval) clearInterval(this.fetchGasPriceInterval) + } + + setGasPrice(gp: GasPriceValue) { + this.cachedGasPrice = gp + } + getPrice() { return this.cachedGasPrice } @@ -140,13 +209,20 @@ export class GasPrice { const speedType = polygonGasPriceKeyMapping[options.speedType] const { maxFee, maxPriorityFee } = json[speedType] - const gasPriceOptions = EIP1559GasPriceWithinLimit( - { - maxFeePerGas: GasPrice.normalizeGasPrice(maxFee), - maxPriorityFeePerGas: GasPrice.normalizeGasPrice(maxPriorityFee), - }, - options.maxFeeLimit - ) + let gasPriceOptions = { + maxFeePerGas: GasPrice.normalizeGasPrice(maxFee), + maxPriorityFeePerGas: GasPrice.normalizeGasPrice(maxPriorityFee), + } + + // Check for possible gas-station invalid response + gasPriceOptions.maxPriorityFeePerGas = BN.min( + toBN(gasPriceOptions.maxFeePerGas), + toBN(gasPriceOptions.maxPriorityFeePerGas) + ).toString(10) + + if (options.maxFeeLimit) { + gasPriceOptions = EIP1559GasPriceWithinLimit(gasPriceOptions, options.maxFeeLimit) + } return gasPriceOptions } diff --git a/zp-relayer/state/rootSet.ts b/zp-relayer/state/rootSet.ts index 70e3f645..3636aa9b 100644 --- a/zp-relayer/state/rootSet.ts +++ b/zp-relayer/state/rootSet.ts @@ -10,7 +10,7 @@ export class RootSet { async remove(indices: string[]) { if (indices.length === 0) return - await this.redis.hdel(this.name, indices) + await this.redis.hdel(this.name, ...indices) } async get(index: string) { diff --git a/zp-relayer/test/depositMemo.json b/zp-relayer/test/depositMemo.json deleted file mode 100644 index e321fcdd..00000000 --- a/zp-relayer/test/depositMemo.json +++ /dev/null @@ -1 +0,0 @@ -"0000000000000000800000001c00d9405f260937aabcc6be1fd2bffe2371ce337cacfd72a4b777ddfd7bf818cd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0ecd431545da5056d2c1fe504d8cc8789fe14e5be6cf9e3fe7c5b4fb1052dbaa0e185b8287890ef5f3d1453fb32c494e680993252ce5a34672144e8daafac6b0226a54661f672176dac316f6c4fcffc920a75c624be1bf72f41adc12ea5c83225e71fc6c55a14f12d225fb34bbcefd5e2951ce79f9c8e3c6dd679af0f5b9ac7f3bedb3877fe0ce61ca0fbc1a8d47455a3f07500a61dcfd353f18e4a8c5ecced016904ee2675a20f5d26e13f1cf294901b71b7f3ecf138ddab4db16a6d2a7b30e0ae4b4501539fd681f085b4be735c2bfe1361bd6067f47323beb3f9d3b5cbea59be9f9843a26c14b321da69eaa22cdaba85d0e32b549afd064854b901e1232c3f64e52b297f7dc2a061d5406ea683e9125af64987f261ab813c9896d733279dcedc20db242edaa5f9d6eb4520a01671d7df636e072eec8ad0f460f38256ffd45d8c8eee15aab2be938294e7f3d18c1facf8032ba1c23593b3c715a0e9ca94fd538c512feacf3dfbf7e3da1cbd575f018079f4c1bfbf41ce7ebd1f87769b17e48fc09cfe92c59805990529d2a94f99cf011871bb0b71686a04f76d7409e019bdef684c67e9174afdbdef9f1d7f0606591afc1bc015d9a90986491757425c5d5938d2ea32a459b8abd00a850777b12754dbdba9503d00cac2ec7f52a0806c20b949cbe90870f392f1818f5e14b4a86ef2070feb575b2b67114d0cdd1452836053c3fc674e1065041ad33eb24ed6193500d8902e3e8a6e3a6ad6d25c7f19942867b4141ffcf430622731c58962b3b5e00200e915b6063e614b75243cb5ea2ba68e96e83628094de3ba32b6bd82b12779d1ec9b6b00b39cc84a96aa08e218b2609a1c5de896fc8784a707163afc7692f54cb4b92b92699efcaf4974f6c6765d3aa72e80bebf52ab4fe459c161a44687ec0980d5bffa0c34cd484f8b50bf7d0707d162a782f086be97809313bde19a890558c3689c76dea8d081fa68c431d0a9868ba43c8e5d1c8af487c67c29d07b4b1383d2966946f7d1dd8bfedf3da1f262044da6778aa6d5303bd53683b561b02009fb42155957b644d412657201b02f2772fc172dbd63e68850d9545046cf1b36d742cbda0e9c5cd54d16c343b295e8bcd6e3eab8a302fe800905c95c37e43bf94f8a4f7bdb07fc7196aafc6f02753bb3667c3641eabbd279d31398381a3552bb24af0a37afee6cb2c8bf2ecae3b2627bfdf8d6b45c6062c94e08afe0faa4eabc7a4777701f3fed222b2f58012c1d282a5494a0b87f5640d1dc212a48fcd6a0674b01aa71d90d6e04832c824a54e14a4945a44737cc9e7e9a3eeb40dbda0554f3eede92180158fd70f983eda2b727c6f6cc456d84d1b73f578abeffc393b6bd6b89c42dfa3a255218ff7d7055abfe2de1579af8cd9c3eb8dfbdc17b052c6c34b76a6c7519bfdbb23bb1be8651eb0a974d18ccbdd71cef603afbf55e8443678a708bfea642656ed412723d86b47e615a467431ceeca402cccf61c0cc73a9afa46e650694872dfbe3842897c701aff803cf318e659aa3d56607191942be234e69ff841bc1e1a92c034b2c51ed80e526c608d3606014cddb8677021638920744bd5770097409e1c95381a6e5efa18f94f468200b948aba0563274d725f5f4103c217861cdcf2b0c88a494fc06b955d5d303cb1ea2e98a6f39a22afb6469bd0b68b083e090235fbb66b4b6923219c8ae6684cdd9f3eba61716a55309bfa6c3cdbe4c9206d8c21e29bcdac5bcea13c891abd07cff31d5b558ea295249c6237893719f6a2075c00ab9c063a9e6b3d3924ce1c69a90dfbe47037f50cd779b0e50e520eb3babc47e54c61b2fb834c14a1bbf569cc8e90150b5258f0ff54f5d9aa1bc0db596f438e969dde63e7bb73f762fd13c947773a9452dd104cb5c5ff90bf35ccdecbd9517cefb26a2a406aa55d4d96177c5fc4d92d57c5b2a80a1410c46221df076d37401e86caddb6a4c7f3e1b8ddf0305fd5e9d394e123434008553609508071e88874afacc4da3318ef8b649f1ebed19cbdad558c01a58d99075c2cd6f9b5b20e7e098692028ad43ee3ce2ff0de7dca96eaed7170a2c8c9388dcb5466f708411851ed75e0c8e168ffa02bf98b333bbb03bbeb97cf091be0279d59c73c1beec1fe1293e8b87ecf97b6b80d4cdd515ef1df3ca5d34ec6f949983dbd6350ea9ba537eb8aa5c94c03df3519398ad2c39f78a4da5cd0d3ff0d79eff1e11a145c486f00dca9cb5db7225f6b7c600099beaec368efa4d60c2016ce8f850881477deacde40190fa8a64ff104688ffefccb224968220e730a891d859e0c3325200bad57cef0816ae5efa44d18a4f9b4a4341bdb5d0bcf66aea4fd5c4763316cb10e4b369dd5a08e34e466b48de80f46c10b29d8d90e072bd7ca4c95ac8a6cad77f4972d2ebb184748470cf961aca1fc34d7e45825ee22fc02d1adc63ae3fe9dceac3c8278d1105da5cb226423441f79a1709b10a8f596b2d66dd24a5edd72f884554d616eea6cd89447ca8c7587e2f634a9ce230b8835633098b0b76649b84767abcfcf4ded4fff3c768ae5864bba9c013d2931fdee2a4cb75ab3a5bc231e840bb122f99dfa7e0f6bb34b95f7c9531f58f9a6ebf888de743c6fd53d9dca84e690b07ef3adf90ed4e75bd19105c2ee87ed4bff1230c040bf1928b4737dc6385591078a0cbdfe14c020ef020477848abc369577fcbe3c23e75040bb1920c6f78d04cb989b39124984602ef90349e11e7ded74556f6210e30acb53538352a5581527f770f751326a1653338b75d01f7fa5c35a1adda5b5c0c3bd1c35395008606f79b30c39ee296032b7e49e32e79980531a8b13c58753b1534e2ce58547acc20184cdb66c8e73f6a38f81d7131a35ddda2a8bfd50f9a21539876267b7bccb5bce3c0b18436fabe252cb84cd74c4305ad40031ebea0194cf9c150b031368194b0e7f9c32f463e98f72c7b898c4f5f2f28fa4db16e2fb7cc9f8e24f990d946d4fb96b78fb798ba0d624ba02faea4434bd5fba3cdff4f3f95b81d8de31de3a65054dd55e0aa08efc22a1a8543eb6821294443f2dc5100ac3cc60d3450e07b74714bd9a903812f22ee967b215edbf9874b7bc3911469458385b671f84c750e5c9beaa3b8e50cfeb13f8006c231be98b50035a1ca1e679da768efeda0e523b596166288150f136fb2ce642c6dc1446ee2cfd47180dbf2df7fc8b5fcaef2ac5a14af8ad724c774137a15d9b25b46feea5f40f4466a91bcee83996aef0acf8b065f28641fd99f47c7713d664ed292becb71056fa37c0295f75deff4f65a6a6c7bb4c90618a1edcf9dc9cac8aa6e0b48fcc399d7a3c7a74caa82a8a11b8509dd935e3661f7f5649a816f3c20e681f483344247dc27ff1f2eb1f83f1e205066431adc5c3774323c310a2a70b79460d66f6736981e612d0eeb09b138216d32c0de909c367d5801b754ecc1938f7044fda80b7699990d1982ae68d04e581f27b2138a355b6e6309c09f0aca149b238368509a0fa11019706519e980556f66620704c6d4ba18e513a359170b0c4f6ae4a3143ed8cb55426d72c8474c93b7a43aed53723270ec8b912f928cc32f5bf03bd52ce77df7ec9066fce291e953d8514964e51fec521033ddfc0d3fc78ca83bd27a2c056aa9b71aa7dbb796d6c82cd6531f42fa3e0d1316e634e73378bdbe739a0aa87f550a68480e0450d7e779b849226c3f758886ac00f5836187c861d6409330710de1bac92a267981cff309bafe902df4361d30e3161efb2049206e5b5d475907bb47ea57965d6b9aad1fe9ac6c11b51e8b30c86881aa9db86d0badad08ce6674be27d83778bdbb1b0b4c84a457f474d591fa8ef31f9c057190d8fff5150b4d8ee204f2f0f0349b63a55c3d17303b5d7768e4f5facd07b735d55bed2e082d955863735cc1c606a447b638ff034b021a14bc8d3e26119e2d7bbb60019258c387f1a0d6169b666b7893144881ce6dc7edc77ad61fb38beacb5ae227c18ba6a64cffad6428bfa52d4a1ca88bdc67112edc3c9847b7c7fb1ceb73883d475561cdb046b9550dda9fb2930ab630a7d7bef933fb019e7dd33325584743f4c25a94bc5b2e061a89eb1406b1a81e395a4280e64d333c464f2ac46db7c9a4074f859f8d3cc86b5b7c3e9fb5d8e26d846f934bd61f5741f37f9c75b5b8005ba33e1b0654ec164d71b058a80d0e4ed87618e87d0c36db3ab549a57ca4aece2b8005e1bb662938cd9c6aec409764369597c486bcb3114cdca2b1926e955badaafc5f38a82aa02af86cd299c8e65d7e703db2433145336c44136963cc2d6b1b7d2e3a2dd5e6dd23f4c528a177bb448beb35b552117053222d98dec7fe8b3ac8e011f967440360096c8e6c336645a612f0b63317ccade1de13dbe75e9652912496c86de2db7b7f7d528c9d11a1cf1939be906530a50cbcbd3712271660d09f8edc7c3772722f5bdd7254bc36739439aa98f6a506c3cd828c599ec1d44a07c9140b051990f20afebbcff830a18a92da31a85cc6f5922ae678ae2331a322525a39ba61cf761196c67b629d2e5d896f0524eda6ec57a6074360197d42ac65e1cd694125975ff41e12ac68bd03db9cd30fa6448a278b7a92dce7c8c1a7664c886a9e78bb3bf10c83b019d2c7ad6fdefced689b0be707ebf2ee1fb7880c5f16dca8ef347a051faaf5477fd288d1db7562dd094047e9f2a0124e731639da9882ac81a055e2bc42996c7a07d2eb9f324cce568c7f4e0e0dadf587561f3cdf30d67eb7343ae3825fd4121bb7e80b914b20bdca4a9d947ec7a7b8354a190a34173936e2ec00560f51d78c4ddc5daa97ab6864cfd32e67cdc00924d2640f04b84d14e49615286f1dad0e1ddca1a11740c7e61b9d1b99a0e071f5bf3277486a13b97b933e24802542fb4ea99012198353f8d16c9353fb651eec601cbbe09ef08c46fb3d512894600274636912d17526765d2b0c8b7ac05965d6f8842cd97b186b00e520ae56135b600023c9eb5104985c14a98d262a311925a1139ff2366294d89773b9aeefe383aa8db049a3fcab7bb889dc40e8f602a3d8b7f01d53c4a258ee76e05a838f51dadf64a1f40780d34eed821e638e209d3bbd1dac52b5738fa66ede9d6c1f293f096447258b1221b7f37041261d0dd75fd43a526ad2a57207eb3358ddabbd167858169276f1bb8aad4d94073c9c0579600569608251bd2c1041f66d583430a2bbf4e702cb2c6b311f0a49ac0190345747184192c685ed64990097523e1593c98538bcf482f989bd61a849e4cec9d69f4b5f6aacf301dc2a4816b14a5742b45d6dfa3d04d8c15aeb0eaddd8a7277fe4c68db1c9102d9fa96578fb0bd1e0c7b5d0d665b0dcfebe05544170f4ece1b3b662ab30946a23db7f6453e5d05989de119867f54d9826639aa5826f816a4de59771bdb5ef08dfaa1799922e0ebf2bf28326495876f8ceb1740e5c219217ecca45358eb0cbc1889e8da2a54753e567a1a56d4e0dc6cf416743b02b9a21225907d8d19f390d5e06e6b4ca6fab3c60eeb012b29350187af9c0020a2e5da014ce226a5bd789a12c44ed0d7c7fec8dfc995c0a6b68b4f9decb4feaffed4c7b2576a622c8a766e23f9b72a10130dd638744987325d509984e495decdc6925b8eb68d6e337c6c0905832c886446cecc0ec17668236245d22e08f44ffbabb758763d4147fbb96f5090d94a77f30a0c421aa80fb745a35cd71875cb18533778e6bbe2fbdac9fd7650beafa9cfe59301fea6848a92259820723d4d38c0dec79826d1712ae2c74247e42c306948c4cac9c6d3a1e24174b91c29a0d98984b4014c2d0c41af5d126d9a2e85f09abacf917500ad007cba6471e627ed8a245a4eb7694dca7197c8d043ae0dfd3c4e2f624f3ba2a1e0f1b731da27bbccf4d5d59eb3d6702018354409cf5cd1af95ce915fce120f91f99cf21d3b9bbbeeaa016620154fab0b4fd175292c3b57e3dc474e3465143bca240a90dea4a4369e34b467fa64a8aebf804dddb3e8efc1fdb4ab02be10f3bc2a95d8a83a1a9797d83ff9b2bcec94044d3fb92647a993bd0251300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a8fa8adb7b548a4f13d01fc0f05563c527180cb46507846e57f5590882406e2700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa623826a697c3b7facb5e5d55bacca14ca4c606e1ad219287bc9dda432fd454a0c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6f25653e682a4d4a03367edf574e385781875c2e71f44445bbe69bf727bb6d42100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d6a85141a8f9bd317f0b21493f08051a2e1e2ab3b1ec4a515306db5a69905a1700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a3f46bdf2fee41303d9fd9210b374cf84610fa0ff45f1d2ab826f1825633a50c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6aa7dc1751a769581048b6f22470b53dfacb83841e37668b6b5211c632c84da1200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6455a3090d5549d9ecc4840eb47d36ae3c8a5c8a288dfef9bb79576ed2e7a7b2d00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa63b76c3533bde2a737f7be7843f7400257a52fa8fd047ac3948c78474e49a6d1200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6eb49e838cfc30f55ee0d67f6032512f7c2eaad06fe8d5722ca9c3f9ee1dc0b0a00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa66c994a7bbb9f5727a6cd711f845c823411e33d0d8163e8bd592d3e96c931890600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6cb3014c085a4ded40c979e8b96c915134c52e7410a34f33bcdd1ee1268a50a2400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6db1e3f813b5c14105eeecd27f357c315d93c6b66baac6c8bfc7f656f9f2f741000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa68abcee28693366542108f2900d2aeb6b1563b4bd7f5d17c1062c7a648e6de60c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6840581adb26bdd8eba8973caf0ea3fcda1e3a6775b2a868619e02fa160a8190e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6858538bc434bd0e778574e7587c2f63b5b904f6659aac05781f93725196b5c2e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa65adc6b8bb67a0dda9c3fffb2ea3eb25ec2b6e385cdc26abf8bc806b77615a11000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa622d82ff057e189afe331de3519c654af619ceb6243f5247b52f84caf68c7290f00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa632bc1f971d2b540313e38f16d3bbb2200fb1a8b075d8839280777c47a91a232600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6c812b83354d3c1059a169889b824463937a10d38c84e53043184c8ce13d04f2b00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa64d24418d45170ad5d99ffe7a3fe31aa95823ce553bfa00390bf994057158fa2d00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d7c9e3bb0aaa0bc4f512dcc7c9cd212d61fde18f6e3e9381a79e91c7ed58601400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6849f7a2657027604493692347258829b2dbb7d79fc4f5f828e52e2444009f32100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa63cbc89381b02f3f5f5cc07b1e72ff3ce23bd9e9b7d5c3c679c38884b352ee92400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6ff05519ebc2e866fde6ff829b86cf21e0e6906ad34549f7b11c4908c40aff42700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6c390800d11c0d3fecef96fc41e563af44a9788242242891db0fd58dcd9ee042b00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6e4f5e120ef8eb3d298825ca4e84d0d599977ed5ab85daefc78c04bfc2b7f330c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6499bb877698bfae590f2ec8f41c4fce3afad0a2abf59296e9c479cfab6a5b92000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6093cb86c1855fe7c029a53a571a795d0d69e7649d09faaec8566433945d9042100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa60e439be58798e1dbbb8295486a35f935119e0a284bd5cd72778d37894d10d02900ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa66a6939b32a922dc579a72ce2eb302c90f9a68b9dea5897622d6967150ed9991600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa63877e4de78c235ca44f34eeed3a6d3bb8b9c93f7a7f88f5fe1d6f7534aa8892f00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa66165c38d1c0111130308b31d28d84382417bbc032e332533818d18476e5ede0a00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa655ce60d57190a788fdd8095533d4ceb073049a1cf771f0d72f9053f360fcf21c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa68588b897238b8ca85c01e6bf5bbb377f2ddf0fad381a0da1d5540dc9babe960100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d23ce1a142ead9d14db07cd653297511345e30ef10c738db2d74e11d3520542400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa65352ab4390088a0bfc5440e0e3e42952ffd82b3055938a2c770b48d549b94d0000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a140a707c4375a89bf79281f6677c852bfb58fb533d16df0422768333d9cc21a00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa615638e9c18aad716e690c44763230e682c21b641b57112a285a0bea7703b2e1100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6cf6272b21e87bae72e265710fe5d835e8c913f3d0187fdbca22d7bffc3f0882100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa69d8d4a597ac80ad2f8b70c110cc4b1b683b8aad43d9ef2b0aa7a1ec293dc181400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6f2c608a6153a38e84e3316542c1552cf2b67ab631075eec1035f0b9d8d29271200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa69e45de37b10b6afbce99b0d264a3cf96b612dc809e90c726290da4ef382cf30c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa62f704ae89c786f6e94a027316cbfdaf60f707d0006912c4026f54a37332e4b0e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa67fc6ef1caf9c880ecba01ede7908d51b10e826d2bbd20d48928e210f4c33a91600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa66aa984e74356f66c4ad45c783631b27f668ae540b49017cb739b2ff09ce2952800ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa609563628e5a2907344733e45cda6b15f08ed35b98d29b894fa7afa04cdf8b30400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d24ca0fa1ec4ce51209ee366c9c6b7357919f5f5ae9d8b33467f42cca6da6f2400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa67dad326302f6c160e6b7eb28966c86751f0c1e85c5854bf9e154501ca02c210200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6ecdecabcc69f76423969dd702e477032235a54324b73aba6f2e71147ba8b410700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa638e4d50c393cfcb9da44739b173f6847dbd8d8cba924a37eb54d95319e761d1400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a9b3c5a0de8afb303157fcc7e49505542648ddbb9c9dbea4c85a503cf5c8822c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa656e7253ff91dc6331c620febf0ec8beb5b956fca85768102cdb04668d1cf3a2700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6177e4aca0ea532ea742213cab2ad4adbc66bd1f3f3253a2e9ea2a4f672c9c42600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa646979be36a3f2036463747a5e0314cd72d9539909776754263b21fa6925c831700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa66a8cad317ee9ad22b13caf66753ca6255ba79deea0002b1937199ff13e67352900ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6b5aef862ce376f182893c90237de41be0aebcf9ca3f05453db9ac513c9bced2b00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6368ffd47914c602412ecda422d0643fb688e9027fcfe974338b4942fe9db310500ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6af993eb7b77c642fc771cf8a8b44aa2a5ff04e025e76fbc617b0bfde39a9d92e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d9660d3ba4ba7a5eece81fb656b364ab628c37929376ed07c44d0e4bb7df6d0a00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6e7e6ba813b31afb59d49c19800860b07027f5b5a27727e74ff89a17206950d2300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa61abd91363a6bddd1b5b973d9fca64a3286d9f546af71e724a091591b25a7650700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa63f9ef0413f28b8bae3dd2726afde89e33486900225900810e7bd9f94920b8f1200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6fdef8d9ab7727d5d5482bf2d5aa7fd69639b2787cfa8322907492b5ae38a202d00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6fc0f82ff19e089881718a1159fafbbe5cfd4285e382e831ad10e5f271c43181700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa67c27b5f91c3cae6ae37c5e6b4126a60b619ba1463408c475dbb7465f3e42451d00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6b7f09dd6e0f77c88a995a096c6d808b7f5d76cd6f87eab5c62d408d3c540750300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6facb519000ceb649e2cd45d9216338130020783802dd3be5755e8fb73290433000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6b5994f1f08f5cacd65b4d07f14655b3745c1180c3c96e5b53af334333d0e590500ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa647172b86404f49e0b9ed5104aebb4ef2c73231339036158666371caaf2daa00e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d33d718e45b0a3708cdc06f718d8e52f3f5ed78d542a3e6af8bd9defeba1e70000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa68e14e0d0d5f15f8d65c14acbb8e3cbc4f675665975294164d5117ec299b7511e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6750571324ca914328158a3b60d3bffe02c0b6017ffc481d4b60084af8e67940e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa61b424346ff88c007c7bad67766a1b9fa231e5fa1908f9d0202ef11382584ff2e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6734f8b6403ff59d25400ffaead93e2360b2cc7e4f00ae1a708ecf7f7bbd6700100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa69874d54a2bce5e6fe0d5a5faf5b91c8a7344e07731eb1f43921cb0e73e31012100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa69923ad70a4be15a75bf5cbb863332d421bc61879c6b15d41399ccfd70c646a1100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa648283165eaff26ddc58a54c419730399fd974a7db85a1ffd63cdce63876b2f1e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa65620e025ff881d221553201d6cba29a5306d4ddc4568e28e863bfeb501bbf22300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6ffccecb5325b5950db05610d127091317f54a57cbae60f52515a48cb9edf0d2700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa65ea4ee2ac6e09c36b062f8360241b665d8ea3e284760cad417b719e6d38f5d3000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6697a8da7c0440e7102ae34ab52227d2af2a5297c8ef1a8c7bc413d2bc2e63a1c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6dcc1cca6c022e2f05e0b9666cc310f52f299a34650655805ad616c0b1f40741800ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa641de1fd90da73c5434e577d9f79f7975c0e9754d5e715cfa281e6bb9a9ff821200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a0dcdca570663136be17eb91b406dc171009a84f6e8725c7956d90e84ab3981a00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d7270586f8449b1be0f1df08ec5fd43972298a717c39289ec65f518eaec6310200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa697907822db580157f1454bd7dbba4bf6d72df2df308b5f8e2925763aa6d44a0c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa61dc30b24dfb02c0b9afd88c3056f5d11e17002f3aa6b88c8094ec51c7853902600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6409d78026b08c1ca126ab443b0b0fcdae4c6fe28e9504f860d0d9a4507b5541800ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa67b0b9827dd1f7c488b3baaeafb2d71c81e3345f6059b170a65f78e1a6719fe1200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6d0160454ad6bbe730572697c75db6fcf41ab7beff5cb6aee62cfc1933dfa590100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6eef515ec23af268eb6e199bc1a4e7c883f7db3b2f7e94639aba76b5d246e3b1700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6c9c1633413ca109f9d33222c659ba845d3d4ce9149b7ac826afca6dbde30410600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa64c921697513ac6d994124a6818e21a0e04af3f1a04fbff8f547c92cf71c0df0200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa60bb284585c8905603115739e18afdf0d0de4a3a5ee738d4bd5459e52e494450000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a5891e242a7ec56a1494ab9f601203bd498ccd50c97f54fda2f02ff5ded5662700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6aeba4a98e0b3db8d94e577bfb7c2d4dabfd6495978caffd5c872ae369a73b91600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa692a607177b0132a504cfd8ffe71d57c3beb350252a2427ebcb7861c598a88f0c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6fa43664ac8c42f3cfcfe5c836fa1bf16cbd4eeeec5a1f2a412f6d32e9522cb1600ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa653a1dcbfc277ba88d151d48126a209a239ce1503f58c5773016fe39b6be8a12300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6b2fe96ebcd3b08ebea82bcf06d79ba655ef337ee69b63824831027beb28bf40e00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6efbfc37bba44e3dbeb7a2fe5997432f769033f0625e6053fe565504f33400a1000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a1c31dac34cb142f1840d68a7c4b34ea94cbbf6b90236820c1d6cae2f14a2e1300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6069ca076c861dfd445fe28756146d009105a215ed3f016287cb9e1264236ea2300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6dc6c7a56053052eb59231c17641c2591a2c54f0ab63e5122f115523334f6e92300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa66196d8a4efecddbc611c969f55c4c1e9dffcb904f096e0307592174614de002c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a8600ff24a55c58e69a08066d18b90c6caec3519e4e020f47e93f491ae17f00200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa610479e5ebaddfd4368cf361a5ff9a6c5247605277371a896f7af7dd240a75b1200ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa686caf30571de77117cf2255ca601a6ef2bb214df0bcf76f18d8e04002ba6b10000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6ff4d8321963c60f88bf5be6ad8a3c8181fcbf822603b4a9494ddade47b17d81300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa67ccf6850f9adc382bc8749708a09f19a25a0ddaa40c67603ad36eb960f002e2900ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa618565c455ab47e7189905111dc57fee438f580e0d847edb31dbf05e3bc3beb1f00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa63b0b7743a31949d6f77f0cb94954d19f78ba2799188ac59dd7f5916be99c0b0700ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa62e1eb517ca7869624b5d4051c3981fe6de9834c41846a1be0a37eee762137d0900ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa61df9d6f8541631bdc6110910d426222df5ff84290bb741b81dab3f1b6625d20100ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa69b66f290ad3952b0de8939167c8f5b2582c6c5d277b80aec9fa23378deb2482900ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6bea9a2ea931ea76b92af936f50525c71c2ad37b9b5c5eb5a701852a90e4df90000ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa64c7582d253b48c1291fa2a95808fdb4936a109bf3457ace90587dc7dcbc8530f00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa677498ca34ba574b024375cc60533f6adf5229f1c876f7697edb4de4c0e6d002500ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6153ff8989d56e552a583dfd7d3cb169b8e11a9771c61642b0b4a0b3fe067890f00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6e3f9dd2cc9e046d74039cd7fbcdc45f329af192c81f31ef950dd15aaec972b0300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa697d48f8b00cd562e5bb6e6d728be22f435235c9896ac0abcab32a858b0ca6b1a00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa625a3a10973118c690c2fecf21ada2229b29957da6747fb6dc4221df6ccf1a61b00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6a90a958358657967be15f155c457863d9f688bf030b04ab290bc8510ef78f72400ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa63332dcfe2741079951a519f7f2ae23a5a49c064afdbb2e11c7b1f10f46d8802f00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa69e160e8465360e5f86a10f83ef23b58ae4237ee9cb577509a566d2c05858122300ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa60f5b0dea38aa8fe09be68ce0a33ff8f1851963a59a94867160d535280ae5c30c00ebdbad0dda9e5949dca717cbec7ce0ef2ec647c66797f684c32a7d85159c150d26b7ab8821510f4bb59c1c81877e214ccdc397dadd16ae8d4c6549da70689ca0638320a49965f060ed1aa6" diff --git a/zp-relayer/test/pool.test.ts b/zp-relayer/test/pool.test.ts deleted file mode 100644 index 368988ff..00000000 --- a/zp-relayer/test/pool.test.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { expect } from 'chai' -import { toBN } from 'web3-utils' -import { EIP1559GasPriceWithinLimit } from '../services/gas-price/GasPrice' -import { checkDeadline } from '../validateTx' - -describe('Pool', () => { - it('correctly calculates fee limit', () => { - const fees = { - maxFeePerGas: '15', - maxPriorityFeePerGas: '7', - } - - expect(EIP1559GasPriceWithinLimit(fees, toBN(100))).to.eql({ - maxFeePerGas: '15', - maxPriorityFeePerGas: '7', - }) - - expect(EIP1559GasPriceWithinLimit(fees, toBN(10))).to.eql({ - maxFeePerGas: '10', - maxPriorityFeePerGas: '7', - }) - - expect(EIP1559GasPriceWithinLimit(fees, toBN(6))).to.eql({ - maxFeePerGas: '6', - maxPriorityFeePerGas: '6', - }) - }) - it('correctly checks deadline', () => { - // curent time + 10 sec - const signedDeadline = toBN(Math.floor(Date.now() / 1000) + 10) - - expect(checkDeadline(signedDeadline, 7)).to.be.null - expect(checkDeadline(signedDeadline, 11)).to.be.instanceOf(Error) - }) -}) diff --git a/zp-relayer/test/unit-tests/GasPrice.test.ts b/zp-relayer/test/unit-tests/GasPrice.test.ts new file mode 100644 index 00000000..82f3dabe --- /dev/null +++ b/zp-relayer/test/unit-tests/GasPrice.test.ts @@ -0,0 +1,61 @@ +import { expect } from 'chai' +import { toBN } from 'web3-utils' +import { EIP1559GasPriceWithinLimit, addExtraGasPrice } from '../../services/gas-price/GasPrice' + +describe('GasPrice', () => { + it('correctly calculates fee limit', () => { + const fees = { + maxFeePerGas: '15', + maxPriorityFeePerGas: '7', + } + + expect(EIP1559GasPriceWithinLimit(fees, toBN(100))).eql({ + maxFeePerGas: '15', + maxPriorityFeePerGas: '7', + }) + + expect(EIP1559GasPriceWithinLimit(fees, toBN(10))).eql({ + maxFeePerGas: '10', + maxPriorityFeePerGas: '7', + }) + + expect(EIP1559GasPriceWithinLimit(fees, toBN(6))).eql({ + maxFeePerGas: '6', + maxPriorityFeePerGas: '6', + }) + }) + it('applies gas fee bump', () => { + let fees = { + maxFeePerGas: '100', + maxPriorityFeePerGas: '50', + } + + expect(addExtraGasPrice(fees)).eql({ + maxFeePerGas: '110', + maxPriorityFeePerGas: '55', + }) + + expect(addExtraGasPrice(fees, 0.1, toBN(100))).eql({ + maxFeePerGas: '100', + maxPriorityFeePerGas: '55', + }) + + expect(addExtraGasPrice(fees, 0.1, toBN(52))).eql({ + maxFeePerGas: '52', + maxPriorityFeePerGas: '52', + }) + + fees = { + maxFeePerGas: '174', + maxPriorityFeePerGas: '56', + } + // Should be rounded correctly + expect(addExtraGasPrice(fees)).eql({ + maxFeePerGas: '191', + maxPriorityFeePerGas: '62', + }) + + // Works with 0 bump + expect(addExtraGasPrice(fees, 0)).eql(fees) + }) +}) diff --git a/zp-relayer/test/unit-tests/validateTx.test.ts b/zp-relayer/test/unit-tests/validateTx.test.ts new file mode 100644 index 00000000..c98cadec --- /dev/null +++ b/zp-relayer/test/unit-tests/validateTx.test.ts @@ -0,0 +1,13 @@ +import { expect } from 'chai' +import { toBN } from 'web3-utils' +import { checkDeadline } from '../../validateTx' + +describe('Validation', () => { + it('correctly checks deadline', () => { + // curent time + 10 sec + const signedDeadline = toBN(Math.floor(Date.now() / 1000) + 10) + + expect(checkDeadline(signedDeadline, 7)).to.be.null + expect(checkDeadline(signedDeadline, 11)).to.be.instanceOf(Error) + }) +}) diff --git a/zp-relayer/tx/signAndSend.ts b/zp-relayer/tx/signAndSend.ts index 9330e7fb..66ad2254 100644 --- a/zp-relayer/tx/signAndSend.ts +++ b/zp-relayer/tx/signAndSend.ts @@ -1,12 +1,15 @@ import Web3 from 'web3' import type { TransactionConfig } from 'web3-core' -export async function signAndSend(txConfig: TransactionConfig, privateKey: string, web3: Web3): Promise { +export async function signTransaction(web3: Web3, txConfig: TransactionConfig, privateKey: string) { const serializedTx = await web3.eth.accounts.signTransaction(txConfig, privateKey) + return [serializedTx.transactionHash as string, serializedTx.rawTransaction as string] +} +export async function sendTransaction(web3: Web3, rawTransaction: string): Promise { return new Promise((res, rej) => - web3.eth - .sendSignedTransaction(serializedTx.rawTransaction as string) + // prettier-ignore + web3.eth.sendSignedTransaction(rawTransaction) .once('transactionHash', res) .once('error', rej) ) diff --git a/zp-relayer/utils/constants.ts b/zp-relayer/utils/constants.ts index abcba235..b2c33546 100644 --- a/zp-relayer/utils/constants.ts +++ b/zp-relayer/utils/constants.ts @@ -4,7 +4,6 @@ const constants = { FALLBACK_RPC_URL_SWITCH_TIMEOUT: 60 * 60 * 1000, TX_QUEUE_NAME: 'tx', SENT_TX_QUEUE_NAME: 'sent', - MAX_SENT_LIMIT: 10, OUTPLUSONE: Constants.OUT + 1, TRANSFER_INDEX_SIZE: 12, ENERGY_SIZE: 28, diff --git a/zp-relayer/utils/helpers.ts b/zp-relayer/utils/helpers.ts index 6c09dae6..3da36a52 100644 --- a/zp-relayer/utils/helpers.ts +++ b/zp-relayer/utils/helpers.ts @@ -112,7 +112,39 @@ export async function withErrorLog(f: () => Promise): Promise { try { return await f() } catch (e) { - logger.error('Found error: %o', e) + logger.error('Found error: %s', (e as Error).message) throw e } } + +function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +export function withLoop(f: () => Promise, timeout: number, supressedErrors: string[] = []): () => Promise { + // @ts-ignore + return async () => { + while (1) { + try { + return await f() + } catch (e) { + const err = e as Error + let isSupressed = false + for (let supressedError of supressedErrors) { + if (err.message.includes(supressedError)) { + isSupressed = true + break + } + } + + if (isSupressed) { + logger.warn('%s', err.message) + } else { + logger.error('Found error %s', err.message) + } + + await sleep(timeout) + } + } + } +} diff --git a/zp-relayer/utils/redisFields.ts b/zp-relayer/utils/redisFields.ts index 1d947078..abbbdd9d 100644 --- a/zp-relayer/utils/redisFields.ts +++ b/zp-relayer/utils/redisFields.ts @@ -44,9 +44,3 @@ export function updateField(key: RelayerKeys, val: any) { export function updateNonce(nonce: number) { return updateField(RelayerKeys.NONCE, nonce) } - -export async function incrNonce() { - const nonce = await redis.incr(RelayerKeys.NONCE) - logger.info(`Incremented nonce to ${nonce}`) - return nonce - 1 -} diff --git a/zp-relayer/validateTx.ts b/zp-relayer/validateTx.ts index 2669e3d9..624e7936 100644 --- a/zp-relayer/validateTx.ts +++ b/zp-relayer/validateTx.ts @@ -264,6 +264,4 @@ export async function validateTx({ txType, rawMemo, txProof, depositSignature }: const limits = await pool.getLimitsFor(userAddress) await checkAssertion(() => checkLimits(limits, delta.tokenAmount)) - - return txData } diff --git a/zp-relayer/workers/poolTxWorker.ts b/zp-relayer/workers/poolTxWorker.ts index 0ce191eb..6cebddb2 100644 --- a/zp-relayer/workers/poolTxWorker.ts +++ b/zp-relayer/workers/poolTxWorker.ts @@ -2,49 +2,59 @@ import { toBN, toWei } from 'web3-utils' import { Job, Worker } from 'bullmq' import { web3 } from '@/services/web3' import { logger } from '@/services/appLogger' -import { TxPayload } from '@/queue/poolTxQueue' -import { TX_QUEUE_NAME, OUTPLUSONE, MAX_SENT_LIMIT } from '@/utils/constants' -import { readNonce, updateField, RelayerKeys, incrNonce, updateNonce } from '@/utils/redisFields' +import { PoolTxResult, TxPayload } from '@/queue/poolTxQueue' +import { TX_QUEUE_NAME, OUTPLUSONE } from '@/utils/constants' +import { readNonce, updateField, RelayerKeys, updateNonce } from '@/utils/redisFields' import { numToHex, truncateMemoTxPrefix, withErrorLog, withMutex } from '@/utils/helpers' -import { signAndSend } from '@/tx/signAndSend' -import { pool } from '@/pool' +import { signTransaction, sendTransaction } from '@/tx/signAndSend' +import { Pool, pool } from '@/pool' import { sentTxQueue } from '@/queue/sentTxQueue' import { processTx } from '@/txProcessor' import config from '@/config' -import { redis } from '@/services/redisClient' -import { validateTx } from '@/validateTx' -import type { EstimationType, GasPrice } from '@/services/gas-price' +import { addExtraGasPrice, EstimationType, GasPrice } from '@/services/gas-price' import type { Mutex } from 'async-mutex' import { getChainId } from '@/utils/web3' import { getTxProofField } from '@/utils/proofInputs' +import type { Redis } from 'ioredis' + +export async function createPoolTxWorker( + gasPrice: GasPrice, + validateTx: (tx: TxPayload, pool: Pool) => Promise, + mutex: Mutex, + redis: Redis +) { + const WORKER_OPTIONS = { + autorun: false, + connection: redis, + concurrency: 1, + } -const WORKER_OPTIONS = { - autorun: false, - connection: redis, - concurrency: 1, -} + let nonce = await readNonce(true) + await updateNonce(nonce) -export async function createPoolTxWorker(gasPrice: GasPrice, mutex: Mutex) { const CHAIN_ID = await getChainId(web3) const poolTxWorkerProcessor = async (job: Job) => { + const sentTxNum = await sentTxQueue.count() + if (sentTxNum >= config.maxSentQueueSize) { + throw new Error('Optimistic state overflow') + } + const txs = job.data const logPrefix = `POOL WORKER: Job ${job.id}:` logger.info('%s processing...', logPrefix) logger.info('Recieved %s txs', txs.length) - const txHashes = [] + const txHashes: [string, string][] = [] for (const tx of txs) { const { gas, amount, rawMemo, txType, txProof } = tx - const txData = await validateTx(tx, pool) + await validateTx(tx, pool) const { data, commitIndex, rootAfter } = await processTx(job.id as string, tx) - const nonce = await incrNonce() logger.info(`${logPrefix} nonce: ${nonce}`) - const gasPriceOptions = gasPrice.getPrice() const txConfig = { data, nonce, @@ -54,14 +64,20 @@ export async function createPoolTxWorker(gasPrice: Gas chainId: CHAIN_ID, } try { - const txHash = await signAndSend( + const gasPriceValue = await gasPrice.fetchOnce() + const gasPriceWithExtra = addExtraGasPrice(gasPriceValue, config.gasPriceSurplus) + const [txHash, rawTransaction] = await signTransaction( + web3, { ...txConfig, - ...gasPriceOptions, + ...gasPriceWithExtra, }, - config.relayerPrivateKey, - web3 + config.relayerPrivateKey ) + await sendTransaction(web3, rawTransaction) + + await updateNonce(++nonce) + logger.debug(`${logPrefix} TX hash ${txHash}`) await updateField(RelayerKeys.TRANSFER_NUM, commitIndex * OUTPLUSONE) @@ -81,21 +97,17 @@ export async function createPoolTxWorker(gasPrice: Gas [poolIndex]: rootAfter, }) - txHashes.push(txHash) - - await sentTxQueue.add( + const sentJob = await sentTxQueue.add( txHash, { - txType, root: rootAfter, outCommit, commitIndex, - txHash, prefixedMemo, nullifier, txConfig, - gasPriceOptions, - txData, + txPayload: tx, + prevAttempts: [[txHash, gasPriceWithExtra]], }, { delay: config.sentTxDelay, @@ -103,10 +115,7 @@ export async function createPoolTxWorker(gasPrice: Gas } ) - const sentTxNum = await sentTxQueue.count() - if (sentTxNum > MAX_SENT_LIMIT) { - await poolTxWorker.pause() - } + txHashes.push([txHash, sentJob.id as string]) } catch (e) { logger.error(`${logPrefix} Send TX failed: ${e}`) throw e @@ -116,8 +125,7 @@ export async function createPoolTxWorker(gasPrice: Gas return txHashes } - await updateNonce(await readNonce(true)) - const poolTxWorker = new Worker( + const poolTxWorker = new Worker( TX_QUEUE_NAME, job => withErrorLog(withMutex(mutex, () => poolTxWorkerProcessor(job))), WORKER_OPTIONS diff --git a/zp-relayer/workers/sentTxWorker.ts b/zp-relayer/workers/sentTxWorker.ts index 3f4e4eb2..dce32318 100644 --- a/zp-relayer/workers/sentTxWorker.ts +++ b/zp-relayer/workers/sentTxWorker.ts @@ -1,75 +1,34 @@ +import type Redis from 'ioredis' import type { Mutex } from 'async-mutex' -import { toBN } from 'web3-utils' -import { Job, Queue, Worker } from 'bullmq' -import { PermittableDepositTxData, TxType } from 'zp-memo-parser' +import type { TransactionReceipt } from 'web3-core' +import { Job, Worker } from 'bullmq' import config from '@/config' import { pool } from '@/pool' import { web3 } from '@/services/web3' import { logger } from '@/services/appLogger' -import { redis } from '@/services/redisClient' -import { GasPrice, EstimationType, chooseGasPriceOptions } from '@/services/gas-price' -import { withErrorLog, withMutex } from '@/utils/helpers' -import { readNonce, updateNonce } from '@/utils/redisFields' +import { GasPrice, EstimationType, chooseGasPriceOptions, addExtraGasPrice } from '@/services/gas-price' +import { withErrorLog, withLoop, withMutex } from '@/utils/helpers' import { OUTPLUSONE, SENT_TX_QUEUE_NAME } from '@/utils/constants' -import { isGasPriceError, isNonceError, isSameTransactionError } from '@/utils/web3Errors' -import { SentTxPayload, sentTxQueue, SentTxResult, SentTxState } from '@/queue/sentTxQueue' -import { signAndSend } from '@/tx/signAndSend' -import { checkAssertion, checkDeadline } from '@/validateTx' - -const token = 'RELAYER' - -const WORKER_OPTIONS = { - autorun: false, - connection: redis, - concurrency: 1, -} +import { isGasPriceError, isSameTransactionError } from '@/utils/web3Errors' +import { SendAttempt, SentTxPayload, sentTxQueue, SentTxResult, SentTxState } from '@/queue/sentTxQueue' +import { sendTransaction, signTransaction } from '@/tx/signAndSend' +import { poolTxQueue } from '@/queue/poolTxQueue' +import { getNonce } from '@/utils/web3' const REVERTED_SET = 'reverted' +const RECHECK_ERROR = 'Waiting for next check' -async function markFailed(ids: string[]) { +async function markFailed(redis: Redis, ids: string[]) { if (ids.length === 0) return await redis.sadd(REVERTED_SET, ids) } -async function checkMarked(id: string) { +async function checkMarked(redis: Redis, id: string) { const inSet = await redis.sismember(REVERTED_SET, id) return Boolean(inSet) } -async function collectBatch(queue: Queue) { - const jobs = await queue.getJobs(['delayed', 'waiting']) - - await Promise.all( - jobs.map(async j => { - // TODO fix "Missing lock for job" error - await j.moveToFailed( - { - message: 'rescheduled', - name: 'RescheduledError', - }, - token - ) - }) - ) - - return jobs -} - async function clearOptimisticState() { - // TODO: a more efficient strategy would be to collect all other jobs - // and move them to 'failed' state as we know they will be reverted - // To do this we need to acquire a lock for each job. Did not find - // an easy way to do that yet. See 'collectBatch' - - // XXX: txs marked as failed potentially could mine - // We should either try to resend them until we are sure - // they have mined or try to make new replacement txs - // with higher gasPrice - const jobs = await sentTxQueue.getJobs(['delayed', 'waiting']) - const ids = jobs.map(j => j.id as string) - logger.info('Marking ids %j as failed', ids) - await markFailed(ids) - logger.info('Rollback optimistic state...') pool.optimisticState.rollbackTo(pool.state) logger.info('Clearing optimistic nullifiers...') @@ -82,22 +41,61 @@ async function clearOptimisticState() { logger.info(`Assert roots are equal: ${root1}, ${root2}, ${root1 === root2}`) } -export async function createSentTxWorker(gasPrice: GasPrice, mutex: Mutex) { +export async function createSentTxWorker(gasPrice: GasPrice, mutex: Mutex, redis: Redis) { + const WORKER_OPTIONS = { + autorun: false, + connection: redis, + concurrency: 1, + } + + async function checkMined( + prevAttempts: SendAttempt[], + txNonce: number + ): Promise<[TransactionReceipt | null, boolean]> { + // Transaction was not mined + const actualNonce = await getNonce(web3, config.relayerAddress) + logger.info('Nonce value from RPC: %d; tx nonce: %d', actualNonce, txNonce) + if (actualNonce <= txNonce) { + return [null, false] + } + + let tx = null + // Iterate in reverse order to check the latest hash first + for (let i = prevAttempts.length - 1; i >= 0; i--) { + const txHash = prevAttempts[i][0] + logger.info('Verifying %s ...', txHash) + tx = await web3.eth.getTransactionReceipt(txHash) + if (tx) break + } + + // Transaction was not mined, but nonce was increased + // Should send for re-processing + if (tx === null) { + logger.warn('Transaction was not mined, but nonce increased; tx should be re-processed') + return [null, true] + } + + return [tx, false] + } + const sentTxWorkerProcessor = async (job: Job) => { const logPrefix = `SENT WORKER: Job ${job.id}:` logger.info('%s processing...', logPrefix) - const { txType, txHash, prefixedMemo, commitIndex, outCommit, nullifier, root, txData } = job.data - - // TODO: it is possible that a tx marked as failed could be stuck - // in the mempool. Worker should either assure that it is mined - // or try to substitute such transaction with another one - if (await checkMarked(job.id as string)) { - logger.info('%s marked as failed, skipping', logPrefix) - return [SentTxState.REVERT, txHash] as SentTxResult + const { prefixedMemo, commitIndex, outCommit, nullifier, root, prevAttempts, txConfig } = job.data + + // Any thrown web3 error will re-trigger re-send loop iteration + const [tx, shouldReprocess] = await checkMined(prevAttempts, txConfig.nonce as number) + // Should always be defined + const [lastHash, lastGasPrice] = prevAttempts.at(-1) as SendAttempt + + if (shouldReprocess) { + logger.info('%s sending this job for re-processing...', logPrefix) + await poolTxQueue.add('reprocess', [job.data.txPayload]) + return [SentTxState.SKIPPED, lastHash, []] as SentTxResult } - const tx = await web3.eth.getTransactionReceipt(txHash) if (tx) { + const txHash = tx.transactionHash // Tx mined if (tx.status) { // Successful @@ -125,81 +123,94 @@ export async function createSentTxWorker(gasPrice: Gas logger.error('Commitments are not equal') } - return [SentTxState.MINED, txHash] as SentTxResult + return [SentTxState.MINED, txHash, []] as SentTxResult } else { // Revert logger.error('%s Transaction %s reverted at block %s', logPrefix, txHash, tx.blockNumber) + // Means that rollback was done previously, no need to do it now + if (await checkMarked(redis, job.id as string)) { + logger.info('%s Job %s marked as failed, skipping', logPrefix, job.id) + return [SentTxState.REVERT, txHash, []] as SentTxResult + } + await clearOptimisticState() - return [SentTxState.REVERT, txHash] as SentTxResult + + // Send all jobs to re-process + // Validation of these jobs will be done in `poolTxWorker` + const waitingJobIds = [] + const reschedulePromises = [] + const waitingJobs = await sentTxQueue.getJobs(['delayed', 'waiting']) + for (let wj of waitingJobs) { + // One of the jobs can be undefined, so we need to check it + // https://github.com/taskforcesh/bullmq/blob/master/src/commands/addJob-8.lua#L142-L143 + if (!wj?.id) continue + waitingJobIds.push(wj.id) + reschedulePromises.push(poolTxQueue.add(txHash, [wj.data.txPayload]).then(j => j.id as string)) + } + logger.info('Marking ids %j as failed', waitingJobIds) + await markFailed(redis, waitingJobIds) + logger.info('%s Rescheduling %d jobs to process...', logPrefix, waitingJobs.length) + const rescheduledIds = await Promise.all(reschedulePromises) + + return [SentTxState.REVERT, txHash, rescheduledIds] as SentTxResult } } else { // Resend with updated gas price - const txConfig = job.data.txConfig + const fetchedGasPrice = await gasPrice.fetchOnce() + const oldWithExtra = addExtraGasPrice(lastGasPrice, config.minGasPriceBumpFactor, null) + const newWithExtra = addExtraGasPrice(fetchedGasPrice, config.gasPriceSurplus, null) - const oldGasPrice = job.data.gasPriceOptions - const fetchedGasPrice = gasPrice.getPrice() + const newGasPrice = chooseGasPriceOptions(oldWithExtra, newWithExtra) - const newGasPrice = chooseGasPriceOptions(oldGasPrice, fetchedGasPrice) - - logger.warn('Tx %s is not mined; updating gasPrice: %o -> %o', txHash, oldGasPrice, newGasPrice) + logger.warn('%s Tx %s is not mined; updating gasPrice: %o -> %o', logPrefix, lastHash, lastGasPrice, newGasPrice) const newTxConfig = { ...txConfig, ...newGasPrice, } + const [newTxHash, rawTransaction] = await signTransaction(web3, newTxConfig, config.relayerPrivateKey) + job.data.prevAttempts.push([newTxHash, newGasPrice]) try { - if (txType === TxType.PERMITTABLE_DEPOSIT) { - const deadline = (txData as PermittableDepositTxData).deadline - await checkAssertion(() => checkDeadline(toBN(deadline), config.permitDeadlineThresholdResend)) - } - - const newTxHash = await signAndSend(newTxConfig, config.relayerPrivateKey, web3) - - // Add updated job - await sentTxQueue.add( - newTxHash, - { - ...job.data, - txHash: newTxHash, - txConfig: newTxConfig, - gasPriceOptions: newGasPrice, - }, - { - priority: txConfig.nonce, - delay: config.sentTxDelay, - } - ) - return [SentTxState.RESEND, newTxHash] as SentTxResult + await sendTransaction(web3, rawTransaction) } catch (e) { const err = e as Error - logger.warn('%s: Tx resend failed for %s: %s', logPrefix, txHash, err.message) - if (isSameTransactionError(err) || isGasPriceError(err)) { - // Force update gas price - gasPrice.start() - } else if (isNonceError(err)) { - await updateNonce(await readNonce(true)) - } else { - // Error can't be handled - logger.error('%s: Error cannot be handled: %o', logPrefix, err) - // Rollback the tree - await clearOptimisticState() - return [SentTxState.FAILED, txHash] as SentTxResult + logger.warn('%s Tx resend failed for %s: %s', logPrefix, lastHash, err.message) + if (isGasPriceError(err) || isSameTransactionError(err)) { + // Tx wasn't sent successfully, but still update last attempt's + // gasPrice to be acccounted in the next iteration + await job.update({ + ...job.data, + }) } - - // Add same job - await sentTxQueue.add(txHash, job.data, { - priority: txConfig.nonce, - delay: config.sentTxDelay, - }) - return [SentTxState.RESEND, txHash] as SentTxResult + // Error should be caught by `withLoop` to re-run job + throw e } + + // Update job + await job.update({ + ...job.data, + txConfig: newTxConfig, + }) + await job.updateProgress({ txHash: newTxHash, gasPrice: newGasPrice }) + + // Tx re-send successful + // Throw error to re-run job after delay and + // check if tx was mined + throw new Error(RECHECK_ERROR) } } const sentTxWorker = new Worker( SENT_TX_QUEUE_NAME, - job => withErrorLog(withMutex(mutex, () => sentTxWorkerProcessor(job))), + job => + withErrorLog( + withLoop( + withMutex(mutex, () => sentTxWorkerProcessor(job)), + config.sentTxDelay, + [RECHECK_ERROR] + ) + ), WORKER_OPTIONS ) From b9b9fa03c2ef7f2c7b568ccef01f8b9d744b1215 Mon Sep 17 00:00:00 2001 From: Leonid Tyurin Date: Wed, 9 Nov 2022 22:48:43 +0400 Subject: [PATCH 2/4] New job status model, sender consistency check, improved transaction hash tracking (#97) --- zp-relayer/endpoints.ts | 93 +++++++++++++++++++++++++----- zp-relayer/queue/sentTxQueue.ts | 3 +- zp-relayer/state/PoolState.ts | 5 ++ zp-relayer/state/jobIdsMapping.ts | 28 +++++++++ zp-relayer/utils/helpers.ts | 11 +++- zp-relayer/validateTx.ts | 44 ++++++++------ zp-relayer/workers/poolTxWorker.ts | 9 +-- zp-relayer/workers/sentTxWorker.ts | 24 ++++++-- 8 files changed, 174 insertions(+), 43 deletions(-) create mode 100644 zp-relayer/state/jobIdsMapping.ts diff --git a/zp-relayer/endpoints.ts b/zp-relayer/endpoints.ts index 0156c614..bae8ab2e 100644 --- a/zp-relayer/endpoints.ts +++ b/zp-relayer/endpoints.ts @@ -11,6 +11,7 @@ import { checkSendTransactionErrors, checkSendTransactionsErrors, } from './validation/validation' +import { sentTxQueue, SentTxState } from './queue/sentTxQueue' async function sendTransactions(req: Request, res: Response, next: NextFunction) { const errors = checkSendTransactionsErrors(req.body) @@ -108,22 +109,84 @@ async function getTransactionsV2(req: Request, res: Response, next: NextFunction } async function getJob(req: Request, res: Response) { + enum JobStatus { + WAITING = 'waiting', + FAILED = 'failed', + SENT = 'sent', + REVERTED = 'reverted', + COMPLETED = 'completed', + } + + interface GetJobResponse { + resolvedJobId: string + createdOn: number + failedReason: null | string + finishedOn: null | number + state: JobStatus + txHash: null | string + } + const jobId = req.params.id - const job = await poolTxQueue.getJob(jobId) - if (job) { - const state = await job.getState() - const txHash = job.returnvalue - const failedReason = job.failedReason - const createdOn = job.timestamp - const finishedOn = job.finishedOn - - res.json({ - state, - txHash, - failedReason, - createdOn, - finishedOn, - }) + + async function getPoolJobState(requestedJobId: string): Promise { + const jobId = await pool.state.jobIdsMapping.get(requestedJobId) + const job = await poolTxQueue.getJob(jobId) + if (!job) return null + + // Default result object + let result: GetJobResponse = { + resolvedJobId: jobId, + createdOn: job.timestamp, + failedReason: null, + finishedOn: null, + state: JobStatus.WAITING, + txHash: null, + } + + const poolJobState = await job.getState() + if (poolJobState === 'completed') { + // Transaction was included in optimistic state, waiting to be mined + const sentJobId = job.returnvalue[0][1] + const sentJob = await sentTxQueue.getJob(sentJobId) + // Should not happen here, but need to verify to be sure + if (!sentJob) throw new Error('Sent job not found') + + const sentJobState = await sentJob.getState() + if (sentJobState === 'waiting' || sentJobState === 'active' || sentJobState === 'delayed') { + // Transaction is in re-send loop + const txHash = sentJob.data.prevAttempts.at(-1)?.[0] + result.state = JobStatus.SENT + result.txHash = txHash || null + } else if (sentJobState === 'completed') { + const [txState, txHash] = sentJob.returnvalue + if (txState === SentTxState.MINED) { + // Transaction mined successfully + result.state = JobStatus.COMPLETED + result.txHash = txHash + result.finishedOn = sentJob.finishedOn || null + } else if (txState === SentTxState.REVERT) { + // Transaction reverted + result.state = JobStatus.REVERTED + result.txHash = txHash + result.finishedOn = sentJob.finishedOn || null + } + } + } else if (poolJobState === 'failed') { + // Either validation or tx sendind failed + result.state = JobStatus.FAILED + result.failedReason = job.failedReason + result.finishedOn = job.finishedOn || null + } + // Other states mean that transaction is either waiting in queue + // or being processed by worker + // So, no need to update `result` object + + return result + } + + const jobState = await getPoolJobState(jobId) + if (jobState) { + res.json(jobState) } else { res.json(`Job ${jobId} not found`) } diff --git a/zp-relayer/queue/sentTxQueue.ts b/zp-relayer/queue/sentTxQueue.ts index 1436db4e..fa6df919 100644 --- a/zp-relayer/queue/sentTxQueue.ts +++ b/zp-relayer/queue/sentTxQueue.ts @@ -7,10 +7,11 @@ import { TxPayload } from './poolTxQueue' export type SendAttempt = [string, GasPriceValue] export interface SentTxPayload { + poolJobId: string root: string outCommit: string commitIndex: number - prefixedMemo: string + truncatedMemo: string txConfig: TransactionConfig nullifier: string txPayload: TxPayload diff --git a/zp-relayer/state/PoolState.ts b/zp-relayer/state/PoolState.ts index 66b1aa6b..9e98d766 100644 --- a/zp-relayer/state/PoolState.ts +++ b/zp-relayer/state/PoolState.ts @@ -4,18 +4,23 @@ import { OUTPLUSONE } from '@/utils/constants' import { MerkleTree, TxStorage, MerkleProof, Constants, Helpers } from 'libzkbob-rs-node' import { NullifierSet } from './nullifierSet' import { RootSet } from './rootSet' +import { JobIdsMapping } from './jobIdsMapping' export class PoolState { private tree: MerkleTree private txs: TxStorage public nullifiers: NullifierSet public roots: RootSet + public jobIdsMapping: JobIdsMapping constructor(private name: string, redis: Redis, path: string) { this.tree = new MerkleTree(`${path}/${name}Tree.db`) this.txs = new TxStorage(`${path}/${name}Txs.db`) this.nullifiers = new NullifierSet(`${name}-nullifiers`, redis) this.roots = new RootSet(`${name}-roots`, redis) + // This structure can be shared among different pool states + // So, use constant name + this.jobIdsMapping = new JobIdsMapping('job-id-mapping', redis) } getVirtualTreeProofInputs(outCommit: string, transferNum?: number) { diff --git a/zp-relayer/state/jobIdsMapping.ts b/zp-relayer/state/jobIdsMapping.ts new file mode 100644 index 00000000..70337193 --- /dev/null +++ b/zp-relayer/state/jobIdsMapping.ts @@ -0,0 +1,28 @@ +import type { Redis } from 'ioredis' + +export class JobIdsMapping { + constructor(public name: string, private redis: Redis) {} + + async add(mapping: Record) { + if (Object.keys(mapping).length === 0) return + await this.redis.hset(this.name, mapping) + } + + async remove(indices: string[]) { + if (indices.length === 0) return + await this.redis.hdel(this.name, ...indices) + } + + async get(id: string): Promise { + const mappedId = await this.redis.hget(this.name, id) + if (mappedId) { + return await this.get(mappedId) + } else { + return id + } + } + + async clear() { + await this.redis.del(this.name) + } +} diff --git a/zp-relayer/utils/helpers.ts b/zp-relayer/utils/helpers.ts index 3da36a52..2efe1716 100644 --- a/zp-relayer/utils/helpers.ts +++ b/zp-relayer/utils/helpers.ts @@ -4,6 +4,7 @@ import { logger } from '@/services/appLogger' import { SnarkProof } from 'libzkbob-rs-node' import { TxType } from 'zp-memo-parser' import type { Mutex } from 'async-mutex' +import { TxValidationError } from '@/validateTx' const S_MASK = toBN('0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff') const S_MAX = toBN('0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0') @@ -91,6 +92,10 @@ export function flattenProof(p: SnarkProof): string { .join('') } +export function buildPrefixedMemo(outCommit: string, txHash: string, truncatedMemo: string) { + return numToHex(toBN(outCommit)).concat(txHash.slice(2)).concat(truncatedMemo) +} + export async function setIntervalAndRun(f: () => Promise | void, interval: number) { const handler = setInterval(f, interval) await f() @@ -112,7 +117,11 @@ export async function withErrorLog(f: () => Promise): Promise { try { return await f() } catch (e) { - logger.error('Found error: %s', (e as Error).message) + if (e instanceof TxValidationError) { + logger.warn('Validation error: %s', (e as Error).message) + } else { + logger.error('Found error: %s', (e as Error).message) + } throw e } } diff --git a/zp-relayer/validateTx.ts b/zp-relayer/validateTx.ts index 624e7936..bd7ee24b 100644 --- a/zp-relayer/validateTx.ts +++ b/zp-relayer/validateTx.ts @@ -19,11 +19,16 @@ const tokenContract = new web3.eth.Contract(TokenAbi as AbiItem[], config.tokenA const ZERO = toBN(0) +export class TxValidationError extends Error { + constructor(message: string) { + super(message) + } +} + type OptionError = Error | null export async function checkAssertion(f: () => Promise | OptionError) { const err = await f() if (err) { - logger.warn('Assertion error: %s', err.message) throw err } } @@ -36,7 +41,7 @@ export async function checkBalance(address: string, minBalance: string) { const balance = await tokenContract.methods.balanceOf(address).call() const res = toBN(balance).gte(toBN(minBalance)) if (!res) { - return new Error('Not enough balance for deposit') + return new TxValidationError('Not enough balance for deposit') } return null } @@ -48,7 +53,7 @@ export function checkCommitment(treeProof: Proof, txProof: Proof) { export function checkProof(txProof: Proof, verify: (p: SnarkProof, i: Array) => boolean) { const res = verify(txProof.proof, txProof.inputs) if (!res) { - return new Error('Incorrect snark proof') + return new TxValidationError('Incorrect snark proof') } return null } @@ -56,12 +61,12 @@ export function checkProof(txProof: Proof, verify: (p: SnarkProof, i: Array config.maxFaucet) { - return new Error('Native amount too high') + return new TxValidationError('Native amount too high') } return null } @@ -97,7 +102,7 @@ export function checkNativeAmount(nativeAmount: BN | null) { export function checkFee(fee: BN) { logger.debug(`Fee: ${fee}`) if (fee.lt(config.relayerFee)) { - return new Error('Fee too low') + return new TxValidationError('Fee too low') } return null } @@ -111,7 +116,7 @@ export function checkDeadline(signedDeadline: BN, threshold: number) { // Check native amount (relayer faucet) const currentTimestamp = new BN(Math.floor(Date.now() / 1000)) if (signedDeadline <= currentTimestamp.addn(threshold)) { - return new Error(`Deadline is expired`) + return new TxValidationError(`Deadline is expired`) } return null } @@ -119,20 +124,20 @@ export function checkDeadline(signedDeadline: BN, threshold: number) { export function checkLimits(limits: Limits, amount: BN) { if (amount.gt(toBN(0))) { if (amount.gt(limits.depositCap)) { - return new Error('Single deposit cap exceeded') + return new TxValidationError('Single deposit cap exceeded') } if (limits.tvl.add(amount).gte(limits.tvlCap)) { - return new Error('Tvl cap exceeded') + return new TxValidationError('Tvl cap exceeded') } if (limits.dailyUserDepositCapUsage.add(amount).gt(limits.dailyUserDepositCap)) { - return new Error('Daily user deposit cap exceeded') + return new TxValidationError('Daily user deposit cap exceeded') } if (limits.dailyDepositCapUsage.add(amount).gt(limits.dailyDepositCap)) { - return new Error('Daily deposit cap exceeded') + return new TxValidationError('Daily deposit cap exceeded') } } else { if (limits.dailyWithdrawalCapUsage.sub(amount).gt(limits.dailyWithdrawalCap)) { - return new Error('Daily withdrawal cap exceeded') + return new TxValidationError('Daily withdrawal cap exceeded') } } return null @@ -140,7 +145,7 @@ export function checkLimits(limits: Limits, amount: BN) { async function checkDepositEnoughBalance(address: string, requiredTokenAmount: BN) { if (requiredTokenAmount.lte(toBN(0))) { - throw new Error('Requested balance check for token amount <= 0') + throw new TxValidationError('Requested balance check for token amount <= 0') } return checkBalance(address, requiredTokenAmount.toString(10)) @@ -156,7 +161,7 @@ async function getRecoveredAddress( // Signature without `0x` prefix, size is 64*2=128 await checkAssertion(() => { if (depositSignature !== null && checkSize(depositSignature, 128)) return null - return new Error('Invalid deposit signature') + return new TxValidationError('Invalid deposit signature size') }) const nullifier = '0x' + numToHex(toBN(proofNullifier)) const sig = unpackSignature(depositSignature as string) @@ -179,8 +184,11 @@ async function getRecoveredAddress( salt: nullifier, } recoveredAddress = recoverSaltedPermit(message, sig) + if (recoveredAddress.toLowerCase() !== owner.toLowerCase()) { + throw new TxValidationError(`Invalid deposit signer; Restored: ${recoveredAddress}; Expected: ${owner}`) + } } else { - throw new Error('Unsupported txtype') + throw new TxValidationError('Unsupported txtype') } return recoveredAddress @@ -207,7 +215,7 @@ async function checkRoot( } if (root !== proofRoot) { - return new Error(`Incorrect root at index ${indexStr}: given ${proofRoot}, expected ${root}`) + return new TxValidationError(`Incorrect root at index ${indexStr}: given ${proofRoot}, expected ${root}`) } // If recieved correct root from contract update cache (only confirmed state) diff --git a/zp-relayer/workers/poolTxWorker.ts b/zp-relayer/workers/poolTxWorker.ts index 6cebddb2..cebfa042 100644 --- a/zp-relayer/workers/poolTxWorker.ts +++ b/zp-relayer/workers/poolTxWorker.ts @@ -5,7 +5,7 @@ import { logger } from '@/services/appLogger' import { PoolTxResult, TxPayload } from '@/queue/poolTxQueue' import { TX_QUEUE_NAME, OUTPLUSONE } from '@/utils/constants' import { readNonce, updateField, RelayerKeys, updateNonce } from '@/utils/redisFields' -import { numToHex, truncateMemoTxPrefix, withErrorLog, withMutex } from '@/utils/helpers' +import { buildPrefixedMemo, truncateMemoTxPrefix, withErrorLog, withMutex } from '@/utils/helpers' import { signTransaction, sendTransaction } from '@/tx/signAndSend' import { Pool, pool } from '@/pool' import { sentTxQueue } from '@/queue/sentTxQueue' @@ -78,7 +78,7 @@ export async function createPoolTxWorker( await updateNonce(++nonce) - logger.debug(`${logPrefix} TX hash ${txHash}`) + logger.info(`${logPrefix} TX hash ${txHash}`) await updateField(RelayerKeys.TRANSFER_NUM, commitIndex * OUTPLUSONE) @@ -86,7 +86,7 @@ export async function createPoolTxWorker( const outCommit = getTxProofField(txProof, 'out_commit') const truncatedMemo = truncateMemoTxPrefix(rawMemo, txType) - const prefixedMemo = numToHex(toBN(outCommit)).concat(txHash.slice(2)).concat(truncatedMemo) + const prefixedMemo = buildPrefixedMemo(outCommit, txHash, truncatedMemo) pool.optimisticState.updateState(commitIndex, outCommit, prefixedMemo) logger.debug('Adding nullifier %s to OS', nullifier) @@ -100,10 +100,11 @@ export async function createPoolTxWorker( const sentJob = await sentTxQueue.add( txHash, { + poolJobId: job.id as string, root: rootAfter, outCommit, commitIndex, - prefixedMemo, + truncatedMemo, nullifier, txConfig, txPayload: tx, diff --git a/zp-relayer/workers/sentTxWorker.ts b/zp-relayer/workers/sentTxWorker.ts index dce32318..2025b2f7 100644 --- a/zp-relayer/workers/sentTxWorker.ts +++ b/zp-relayer/workers/sentTxWorker.ts @@ -7,7 +7,7 @@ import { pool } from '@/pool' import { web3 } from '@/services/web3' import { logger } from '@/services/appLogger' import { GasPrice, EstimationType, chooseGasPriceOptions, addExtraGasPrice } from '@/services/gas-price' -import { withErrorLog, withLoop, withMutex } from '@/utils/helpers' +import { buildPrefixedMemo, withErrorLog, withLoop, withMutex } from '@/utils/helpers' import { OUTPLUSONE, SENT_TX_QUEUE_NAME } from '@/utils/constants' import { isGasPriceError, isSameTransactionError } from '@/utils/web3Errors' import { SendAttempt, SentTxPayload, sentTxQueue, SentTxResult, SentTxState } from '@/queue/sentTxQueue' @@ -81,7 +81,7 @@ export async function createSentTxWorker(gasPrice: Gas const sentTxWorkerProcessor = async (job: Job) => { const logPrefix = `SENT WORKER: Job ${job.id}:` logger.info('%s processing...', logPrefix) - const { prefixedMemo, commitIndex, outCommit, nullifier, root, prevAttempts, txConfig } = job.data + const { truncatedMemo, commitIndex, outCommit, nullifier, root, prevAttempts, txConfig } = job.data // Any thrown web3 error will re-trigger re-send loop iteration const [tx, shouldReprocess] = await checkMined(prevAttempts, txConfig.nonce as number) @@ -99,9 +99,12 @@ export async function createSentTxWorker(gasPrice: Gas // Tx mined if (tx.status) { // Successful - logger.debug('%s Transaction %s was successfully mined at block %s', logPrefix, txHash, tx.blockNumber) + logger.info('%s Transaction %s was successfully mined at block %s', logPrefix, txHash, tx.blockNumber) + const prefixedMemo = buildPrefixedMemo(outCommit, txHash, truncatedMemo) pool.state.updateState(commitIndex, outCommit, prefixedMemo) + // Update tx hash in optimistic state tx db + pool.optimisticState.addTx(commitIndex * OUTPLUSONE, Buffer.from(prefixedMemo, 'hex')) // Add nullifer to confirmed state and remove from optimistic one logger.info('Adding nullifier %s to PS', nullifier) @@ -140,18 +143,26 @@ export async function createSentTxWorker(gasPrice: Gas // Validation of these jobs will be done in `poolTxWorker` const waitingJobIds = [] const reschedulePromises = [] + const newPoolJobIdMapping: Record = {} const waitingJobs = await sentTxQueue.getJobs(['delayed', 'waiting']) for (let wj of waitingJobs) { // One of the jobs can be undefined, so we need to check it // https://github.com/taskforcesh/bullmq/blob/master/src/commands/addJob-8.lua#L142-L143 if (!wj?.id) continue waitingJobIds.push(wj.id) - reschedulePromises.push(poolTxQueue.add(txHash, [wj.data.txPayload]).then(j => j.id as string)) + const reschedulePromise = poolTxQueue.add(txHash, [wj.data.txPayload]).then(j => { + const newPoolJobId = j.id as string + newPoolJobIdMapping[wj.data.poolJobId] = newPoolJobId + return newPoolJobId + }) + reschedulePromises.push(reschedulePromise) } logger.info('Marking ids %j as failed', waitingJobIds) await markFailed(redis, waitingJobIds) logger.info('%s Rescheduling %d jobs to process...', logPrefix, waitingJobs.length) const rescheduledIds = await Promise.all(reschedulePromises) + logger.info('%s Update pool job id mapping %j ...', logPrefix, newPoolJobIdMapping) + await pool.state.jobIdsMapping.add(newPoolJobIdMapping) return [SentTxState.REVERT, txHash, rescheduledIds] as SentTxResult } @@ -174,6 +185,7 @@ export async function createSentTxWorker(gasPrice: Gas job.data.prevAttempts.push([newTxHash, newGasPrice]) try { await sendTransaction(web3, rawTransaction) + logger.info(`${logPrefix} Re-send tx; New hash: ${newTxHash}`) } catch (e) { const err = e as Error logger.warn('%s Tx resend failed for %s: %s', logPrefix, lastHash, err.message) @@ -188,6 +200,10 @@ export async function createSentTxWorker(gasPrice: Gas throw e } + // Overwrite old tx recorded in optimistic state db with new tx hash + const prefixedMemo = buildPrefixedMemo(outCommit, newTxHash, truncatedMemo) + pool.optimisticState.addTx(commitIndex * OUTPLUSONE, Buffer.from(prefixedMemo, 'hex')) + // Update job await job.update({ ...job.data, From e1bddee29b0e7f54fa71d1a7b9052688d5ad5073 Mon Sep 17 00:00:00 2001 From: Leonid Tyurin Date: Thu, 10 Nov 2022 22:34:03 +0400 Subject: [PATCH 3/4] Fix possible completed job state issue (#99) --- zp-relayer/endpoints.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/zp-relayer/endpoints.ts b/zp-relayer/endpoints.ts index bae8ab2e..76836fe3 100644 --- a/zp-relayer/endpoints.ts +++ b/zp-relayer/endpoints.ts @@ -130,7 +130,7 @@ async function getJob(req: Request, res: Response) { async function getPoolJobState(requestedJobId: string): Promise { const jobId = await pool.state.jobIdsMapping.get(requestedJobId) - const job = await poolTxQueue.getJob(jobId) + let job = await poolTxQueue.getJob(jobId) if (!job) return null // Default result object @@ -146,8 +146,13 @@ async function getJob(req: Request, res: Response) { const poolJobState = await job.getState() if (poolJobState === 'completed') { // Transaction was included in optimistic state, waiting to be mined + if (job.returnvalue === null) { + job = await poolTxQueue.getJob(jobId) + // Sanity check + if (!job || job.returnvalue === null) throw new Error('Internal job inconsistency') + } const sentJobId = job.returnvalue[0][1] - const sentJob = await sentTxQueue.getJob(sentJobId) + let sentJob = await sentTxQueue.getJob(sentJobId) // Should not happen here, but need to verify to be sure if (!sentJob) throw new Error('Sent job not found') @@ -158,6 +163,11 @@ async function getJob(req: Request, res: Response) { result.state = JobStatus.SENT result.txHash = txHash || null } else if (sentJobState === 'completed') { + if (sentJob.returnvalue === null) { + sentJob = await sentTxQueue.getJob(sentJobId) + // Sanity check + if (!sentJob || sentJob.returnvalue === null) throw new Error('Internal job inconsistency') + } const [txState, txHash] = sentJob.returnvalue if (txState === SentTxState.MINED) { // Transaction mined successfully @@ -173,6 +183,11 @@ async function getJob(req: Request, res: Response) { } } else if (poolJobState === 'failed') { // Either validation or tx sendind failed + if (!job.finishedOn) { + job = await poolTxQueue.getJob(jobId) + // Sanity check + if (!job || !job.finishedOn) throw new Error('Internal job inconsistency') + } result.state = JobStatus.FAILED result.failedReason = job.failedReason result.finishedOn = job.finishedOn || null From 13b3f4bbc31dd8961fe0cce04ea9fa714631cfe8 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Mon, 21 Nov 2022 12:34:38 +0300 Subject: [PATCH 4/4] Bump package version to 2.1.0 (#102) --- zp-relayer/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zp-relayer/package.json b/zp-relayer/package.json index 6eeb3e79..1eb5245c 100644 --- a/zp-relayer/package.json +++ b/zp-relayer/package.json @@ -1,6 +1,6 @@ { "name": "zp-relayer", - "version": "2.0.0", + "version": "2.1.0", "main": "build/index.js", "types": "build/index.d.ts", "scripts": {