From 3d1d50ac819d78aee83656898d24a352e0cb5df3 Mon Sep 17 00:00:00 2001 From: Maxim Andreev Date: Sat, 25 Nov 2023 10:54:42 +0300 Subject: [PATCH] Initial commit --- .gitattributes | 1 + .github/workflows/release.yml | 75 ++ .gitignore | 10 + LICENSE | 21 + README.md | 126 ++ benchmark/Makefile | 38 + benchmark/README.md | 80 ++ benchmark/compare.py | 143 ++ benchmark/datasets/largest1k.tar.gz | 3 + benchmark/datasets/random50k.tar.gz | 3 + benchmark/datasets/vyper.tar.gz | 3 + benchmark/download.py | 98 ++ benchmark/index.html | 141 ++ benchmark/providers/etherscan/Dockerfile | 5 + benchmark/providers/etherscan/main.py | 46 + benchmark/providers/evmole-js/Dockerfile | 5 + benchmark/providers/evmole-js/main.mjs | 22 + benchmark/providers/evmole-py/Dockerfile | 5 + benchmark/providers/evmole-py/main.py | 21 + benchmark/providers/simple/Dockerfile | 4 + benchmark/providers/simple/main.py | 33 + benchmark/providers/whatsabi/Dockerfile | 5 + benchmark/providers/whatsabi/main.mjs | 22 + benchmark/results/.gitkeep | 0 evmole/README.md | 3 + evmole/__init__.py | 1 + evmole/evm/__init__.py | 0 evmole/evm/memory.py | 36 + evmole/evm/opcodes.py | 172 +++ evmole/evm/stack.py | 31 + evmole/evm/vm.py | 169 +++ evmole/selectors.py | 92 ++ examples/example.sol | 10 + examples/javascript.mjs | 14 + examples/python.py | 13 + js/.eslintrc.cjs | 27 + js/.prettierrc | 5 + js/README.md | 3 + js/package-lock.json | 1541 ++++++++++++++++++++++ js/package.json | 29 + js/src/evm/memory.js | 53 + js/src/evm/opcodes.js | 333 +++++ js/src/evm/stack.js | 38 + js/src/evm/vm.js | 223 ++++ js/src/index.d.ts | 4 + js/src/index.js | 121 ++ js/src/utils.js | 40 + poetry.lock | 707 ++++++++++ poetry.toml | 2 + pyproject.toml | 38 + tests/memory_test.py | 14 + tests/stack_test.py | 16 + 52 files changed, 4645 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 benchmark/Makefile create mode 100644 benchmark/README.md create mode 100644 benchmark/compare.py create mode 100644 benchmark/datasets/largest1k.tar.gz create mode 100644 benchmark/datasets/random50k.tar.gz create mode 100644 benchmark/datasets/vyper.tar.gz create mode 100644 benchmark/download.py create mode 100644 benchmark/index.html create mode 100644 benchmark/providers/etherscan/Dockerfile create mode 100644 benchmark/providers/etherscan/main.py create mode 100644 benchmark/providers/evmole-js/Dockerfile create mode 100644 benchmark/providers/evmole-js/main.mjs create mode 100644 benchmark/providers/evmole-py/Dockerfile create mode 100644 benchmark/providers/evmole-py/main.py create mode 100644 benchmark/providers/simple/Dockerfile create mode 100644 benchmark/providers/simple/main.py create mode 100644 benchmark/providers/whatsabi/Dockerfile create mode 100644 benchmark/providers/whatsabi/main.mjs create mode 100644 benchmark/results/.gitkeep create mode 100644 evmole/README.md create mode 100644 evmole/__init__.py create mode 100644 evmole/evm/__init__.py create mode 100644 evmole/evm/memory.py create mode 100644 evmole/evm/opcodes.py create mode 100644 evmole/evm/stack.py create mode 100644 evmole/evm/vm.py create mode 100644 evmole/selectors.py create mode 100644 examples/example.sol create mode 100644 examples/javascript.mjs create mode 100644 examples/python.py create mode 100644 js/.eslintrc.cjs create mode 100644 js/.prettierrc create mode 100644 js/README.md create mode 100644 js/package-lock.json create mode 100644 js/package.json create mode 100644 js/src/evm/memory.js create mode 100644 js/src/evm/opcodes.js create mode 100644 js/src/evm/stack.js create mode 100644 js/src/evm/vm.js create mode 100644 js/src/index.d.ts create mode 100644 js/src/index.js create mode 100644 js/src/utils.js create mode 100644 poetry.lock create mode 100644 poetry.toml create mode 100644 pyproject.toml create mode 100644 tests/memory_test.py create mode 100644 tests/stack_test.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..18bdf0d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +benchmark/datasets/*.tar.gz filter=lfs diff=lfs merge=lfs -text diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8742f84 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,75 @@ +name: release + +permissions: + contents: write + +on: + push: + tags: + - '*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # need tags to generate release notes + + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install NodeJS + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install python poetry + run: | + curl -sSL https://install.python-poetry.org | python - + echo "$HOME/.poetry/bin" >> $GITHUB_PATH + + - name: Python - check and build + id: buildpy + run: | + poetry install + poetry run ruff evmole + poetry run black --check evmole + poetry build + echo "wheel_name=evmole-${GITHUB_REF#refs/tags/}-py3-none-any.whl" >> $GITHUB_OUTPUT + + + - name: NodeJS - check and build + id: buildjs + run: | + cd js/ + npm ci + npm run build + npm pack + echo "tarball_name=evmole-${GITHUB_REF#refs/tags/}.tgz" >> $GITHUB_OUTPUT + + - name: Generate Release Notes + run: | + echo '## Changes since previous release:' > changelog.md + git log --oneline $(git describe --tags --abbrev=0 HEAD^)..HEAD --pretty=format:"- [%h](https://github.com/cdump/evmole/commit/%H) %s" >> changelog.md + + - name: Release + uses: softprops/action-gh-release@v1 + with: + name: Release ${{ github.ref_name }} + draft: false + prerelease: false + body_path: changelog.md + files: | + dist/${{ steps.buildpy.outputs.wheel_name }} + js/${{ steps.buildjs.outputs.tarball_name }} + + - name: Publish to NPM and PyPI + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + poetry publish + npm publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9907607 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +benchmark/datasets/* +!benchmark/datasets/*.tar.gz + +benchmark/results/* +!benchmark/results/.gitkeep + +__pycache__/ +node_modules/ + +dist/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5f31d41 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Maxim Andreev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ded1973 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# EVMole + +[![PyPI](https://img.shields.io/pypi/v/evmole)](https://pypi.org/project/evmole) +[![npm](https://img.shields.io/npm/v/evmole)](https://www.npmjs.com/package/evmole) +[![license](https://img.shields.io/github/license/cdump/evmole)](./LICENSE) + +Extracts [function selectors](https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector) from EVM bytecode, even for unverified contracts. + +- Python & JavaScript implementations +- Clean code with zero dependencies +- [Faster and more accurate](#Benchmark) than other tools +- Tested on Solidity and Vyper compiled contracts + +## Usage + +### JavaScript +```sh +$ npm i evmole +``` +```javascript +import {functionSelectors} from 'evmole' + +const code = '0x6080604052600436106025575f3560e01c8063b69ef8a8146029578063d0e30db014604d575b5f80fd5b3480156033575f80fd5b50603b5f5481565b60405190815260200160405180910390f35b60536055565b005b345f8082825460639190606a565b9091555050565b80820180821115608857634e487b7160e01b5f52601160045260245ffd5b9291505056fea2646970667358221220354240f63068d555e9b817619001b0dff6ea630d137edc1a640dae8e3ebb959864736f6c63430008170033' +console.log( functionSelectors(code) ) +// Output(list): [ 'b69ef8a8', 'd0e30db0' ] +``` + +### Python +```sh +$ pip install evmole +``` +```python +from evmole import function_selectors + +code = '0x6080604052600436106025575f3560e01c8063b69ef8a8146029578063d0e30db014604d575b5f80fd5b3480156033575f80fd5b50603b5f5481565b60405190815260200160405180910390f35b60536055565b005b345f8082825460639190606a565b9091555050565b80820180821115608857634e487b7160e01b5f52601160045260245ffd5b9291505056fea2646970667358221220354240f63068d555e9b817619001b0dff6ea630d137edc1a640dae8e3ebb959864736f6c63430008170033' +print( function_selectors(code) ) +# Output(list): ['b69ef8a8', 'd0e30db0'] +``` + +See [examples](./examples) for more + +## Benchmark + +FP/FN - [False Positive/False Negative](https://en.wikipedia.org/wiki/False_positives_and_false_negatives) errors; smaller is better + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Datasetsimplewhatsabievmole-js (py)
largest1k
1000 contracts
24427 functions
FP/FN contracts:95 / 938 / 81 / 0 :1st_place_medal:
FP/FN functions:749 / 1238 / 8 :1st_place_medal: :2nd_place_medal:192 / 0 :2nd_place_medal: :1st_place_medal:
Time:2.06s3.8s1.99s (2.09s) :rocket:
random50k
50000 contracts
1171102 functions
FP/FN contracts:4136 / 77251 / 311 / 9 :1st_place_medal:
FP/FN functions:14652 / 96261 / 323 / 10 :1st_place_medal:
Time:32.3s71.13s25.63s (33.56s) :rocket:
vyper
780 contracts
21244 functions
FP/FN contracts:185 / 480178 / 7800 / 0 :1st_place_medal:
FP/FN functions:197 / 12971181 / 212440 / 0 :1st_place_medal:
Time:1.71s2.52s1.58s (1.8s) :rocket:
+ +See [benchmark/README.md](./benchmark/) for the methodology and commands to reproduce these results + + +## How it works + +Short: Executes code with a custom EVM and traces CALLDATA usage. + +Long: TODO + + +## License +MIT diff --git a/benchmark/Makefile b/benchmark/Makefile new file mode 100644 index 0000000..201b86e --- /dev/null +++ b/benchmark/Makefile @@ -0,0 +1,38 @@ +PROVIDERS ?= etherscan simple whatsabi evmole-py evmole-js +DATASETS ?= largest1k random50k vyper +DOCKER ?= docker +DOCKER_CPUS ?= 1 +DOCKER_PREFIX ?= evmole-bench + +DATASET=$(shell pwd)/datasets +RES=$(shell pwd)/results + +BUILD_TARGETS=$(addsuffix .build, $(PROVIDERS)) +RUN_TARGETS=$(foreach p,$(PROVIDERS),$(addprefix $(p)/, $(DATASETS))) +UNPACK_TARGETS=$(foreach d,$(DATASETS),$(addprefix datasets/, $(d))) + +benchmark: build run + +build: $(BUILD_TARGETS) + +run: $(RUN_TARGETS) + +$(BUILD_TARGETS): + $(info [*] Building $(basename $@)...) + # special hack for evmole: + [ "$@" = "evmole-py.build" ] && cp -r ../evmole providers/evmole-py/ || true + [ "$@" = "evmole-js.build" ] && cp -r ../js providers/evmole-js/ || true + $(DOCKER) build -t $(DOCKER_PREFIX)-$(basename $@) providers/$(basename $@) + [ "$@" = "evmole-py.build" ] && rm -rf providers/evmole-py/evmole || true + [ "$@" = "evmole-js.build" ] && rm -rf providers/evmole-js/js || true + +$(UNPACK_TARGETS): + $(info [*] Unpacking $@...) + tar -C datasets/ -zxf $@.tar.gz + +.SECONDEXPANSION: +$(RUN_TARGETS): datasets/$$(notdir $$@) + $(info [*] Running $@...) + /bin/time -f '%e' $(DOCKER) run --cpus=$(DOCKER_CPUS) --rm -v $(DATASET)/$(notdir $@):/dataset -v $(RES):/mnt -it $(DOCKER_PREFIX)-$(subst /,,$(dir $@)) /dataset /mnt/$(subst /,_,$@).json 2> $(RES)/$(subst /,_,$@).time + +.PHONY: benchmark build run $(BUILD_TARGETS) $(RUN_TARGETS) diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 0000000..837c7b3 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,80 @@ +# Benchmarks + +Test accuracy and speed of different function-signature extractors + +For results, refer to the [main README.md](../README.md#Benchmark). + +## Methodology +1. Get N Etherscan-verified contracts, save the bytecode and ABI to `datasets/NAME/ADDR.json`. +2. Extract function signatures from the bytecode. Each tool runs inside a Docker container and is limited to 1 CPU (see `providers/NAME` and `Makefile`). +3. Assume selectors from Etherscan's ABI as ground truth. +4. Compare the results with it and count [False Positives and False Negatives](https://en.wikipedia.org/wiki/False_positives_and_false_negatives). + +## Reproduce +Set the performance mode using `sudo cpupower frequency-set -g performance` and run `make` ([GNU Make](https://www.gnu.org/software/make/)) inside the `benchmark/` directory. + +To use [Podman](https://podman.io/) instead of Docker: `DOCKER=podman make` + + +You can run only specific step; for example: +```sh +# Only build docker-images +$ make build + +# Only run tests +$ make run + +# Build `etherscan` docker image +$ make etherscan.build + +# Run `etherscan` on dataset `largest1k` +$ make etherscan/largest1k +``` + +To process results run `compare.py`: +```sh +$ python3 compare.py + +# compare in web-browser +$ ../.venv/bin/python3 compare.py --web-listen 127.0.0.1:8080 +``` + + +## How datasets/ was constructed + +1. Clone [tintinweb/smart-contract-sanctuary](https://github.com/tintinweb/smart-contract-sanctuary) + +2. Find all solidity contracts: +```sh +$ cd smart-contract-sanctuary/ethereum/contracts/mainnet/ + +# (contract_size_in_bytes) (contract_file_path) +$ find ./ -name "*.sol" -printf "%s %p\n" > all.txt +``` + +3. Get ~1200 largest (by size) contracts: +```sh +$ cat all.txt | sort -rn | head -n 1200 | cut -d'/' -f3 | cut -d'_' -f1 > top.txt +``` + +4. Get ~55.000 random contracts +```sh +$ cat all.txt | cut -d'/' -f3 | cut -d'_' -f1 | sort -u | shuf | head -n 55000 > random.txt +``` + +5. Get all vyper contracts: +```sh +$ find ./ -type f -name '*.vy' | cut -d'/' -f3 | cut -d'_' -f1 > vyper.txt +``` + +6. Download contracts code & abi: +```sh +$ poetry run python3 datasets/download.py --etherscan-api-key=CHANGE_ME --addrs-list=top.txt --out-dir=datasets/largest_1k --limit=1000 --code-regexp='^0x(?!73).' +$ poetry run python3 datasets/download.py --etherscan-api-key=CHANGE_ME --addrs-list=random.txt --out-dir=datasets/random_10k --limit=10000 --code-regexp='^0x(?!73).' +$ poetry run python3 datasets/download.py --etherscan-api-key=CHANGE_ME --addrs-list=vyper.txt --out-dir=datasets/vyper --code-regexp='^0x(?!73).' +``` + +We use `--code-regexp='^0x(?!73).'` to: +1. Skip contract with empty code (`{"code": "0x",`) - these are self-destructed contracts. +2. Skip contract with code starting from `0x73` (`PUSH20` opcode). +Compiled Solidity libraries [begins with this code](https://docs.soliditylang.org/en/v0.8.23/contracts.html#call-protection-for-libraries), and because [Non-storage structs are referred to by their fully qualified name](https://docs.soliditylang.org/en/v0.8.23/contracts.html#function-signatures-and-selectors-in-libraries) it's not yet supported by our reference Etherscan extractor (`providers/etherscan`). This issue may be fixed later. diff --git a/benchmark/compare.py b/benchmark/compare.py new file mode 100644 index 0000000..504ac42 --- /dev/null +++ b/benchmark/compare.py @@ -0,0 +1,143 @@ +import argparse +import json +import pathlib + +def process_dataset(dname: str, providers: list[str], results_dir: str): + pdata = [] + ptimes = [] + for pname in providers: + with open(f'{results_dir}/{pname}_{dname}.json', 'r') as fh: + pdata.append(json.load(fh)) + with open(f'{results_dir}/{pname}_{dname}.time', 'r') as fh: + ptimes.append(float(fh.read())) + + ret = [] + for fname, gt in pdata[0].items(): + gt_set = set(gt) + data = [] + for i in range(1, len(providers)): # skip ground_truth provider + d = set(pdata[i].get(fname, [])) + fp = list(d - gt_set) + fn = list(gt_set - d) + data.append([fp, fn]) + ret.append({ + 'addr': fname[2:-5], # '0xFF.json' => 'FF' + 'ground_truth': gt, + 'data': data, + }) + return {'dataset': dname, 'results': ret, 'timings': ptimes[1:]} + + +def markdown(providers: list[str], all_results: list): + # :1st_place_medal: :rocket: :zap: + print('') + print(' ') + print(' ') + print(' ') + for name in providers[1:]: + print(f' ') + print(' ') + for dataset_idx, dataset_result in enumerate(all_results): + dataset_name = dataset_result['dataset'] + cnt_contracts = len(dataset_result['results']) + cnt_signatures = sum(len(x['ground_truth']) for x in dataset_result['results']) + print(' ') + print(f' ') + print(' ') + for idx in range(0, len(providers) - 1): # skip ground_truth provider + fp_contracts = sum(len(x['data'][idx][0]) > 0 for x in dataset_result['results']) + fn_contracts = sum(len(x['data'][idx][1]) > 0 for x in dataset_result['results']) + print(f' ') + print(' ') + print(' ') + print(' ') + for idx in range(0, len(providers) - 1): # skip ground_truth provider + fp_signatures = sum(len(x['data'][idx][0]) for x in dataset_result['results']) + fn_signatures = sum(len(x['data'][idx][1]) for x in dataset_result['results']) + print(f' ') + print(' ') + print(' ') + print(' ') + for idx in range(0, len(providers) - 1): # skip ground_truth provider + print(f' ') + print(' ') + if dataset_idx != len(all_results) - 1: + print(f' ') + print('
Dataset{name}
{dataset_name}
{cnt_contracts} contracts
{cnt_signatures} functions
FP/FN contracts:{fp_contracts} / {fn_contracts}
FP/FN functions:{fp_signatures} / {fn_signatures}
Time:{dataset_result["timings"][idx]}s
') + + +def serve_web(listen_host: str, listen_port:int, providers: list[str], all_results: list): + """ + { + "providers": ["etherscan", "a", "b"], + "results": [ + { + "dataset": "name", + "timings": [1.2, 0.33], # for every provider + "results": [ + { + "addr": "address", + "ground_truth": ["00aabbcc", "ddeeff22"], + "data": [ + [["fp"], ["fn"]], # 1st provider errors + [["fp"], ["fn"]], # 2nd provider errors + ], + }, + ] + } + ], + } + """ + data = {'providers': providers, 'results': all_results} + json_data = json.dumps(data, separators=(',', ':')) + async def handle_index(_): + return web.FileResponse(pathlib.Path(__file__).parent / 'index.html') + async def handle_res(_): + return web.Response(body=json_data, headers={'Content-Type': 'application/json'}) + app = web.Application() + app.add_routes([web.get('/', handle_index), web.get('/res.json', handle_res)]) + web.run_app(app, host=listen_host, port=listen_port) + + +def show(providers: list[str], all_results: list): + for dataset_result in all_results: + cnt_contracts = len(dataset_result['results']) + cnt_signatures = sum(len(x['ground_truth']) for x in dataset_result['results']) + for provider_idx, name in enumerate(providers[1:]): + fp_signatures = sum(len(x['data'][provider_idx][0]) for x in dataset_result['results']) + fn_signatures = sum(len(x['data'][provider_idx][1]) for x in dataset_result['results']) + fp_contracts = sum(len(x['data'][provider_idx][0]) > 0 for x in dataset_result['results']) + fn_contracts = sum(len(x['data'][provider_idx][1]) > 0 for x in dataset_result['results']) + print(f'dataset {dataset_result["dataset"]} ({cnt_contracts} contracts, {cnt_signatures} signatures), {name}:') + print(f' time: {dataset_result["timings"][provider_idx]}s') + print(f' False Positive: {fp_signatures} signatures, {fp_contracts} contracts') + print(f' False Negative: {fn_signatures} signatures, {fn_contracts} contracts') + print('') + print('') + pass + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--results-dir', type=str, default=pathlib.Path(__file__).parent / 'results', help='results directory') + parser.add_argument('--providers', nargs='+', default=['etherscan', 'simple', 'whatsabi', 'evmole-py', 'evmole-js']) + parser.add_argument('--datasets', nargs='+', default=['largest1k', 'random50k', 'vyper']) + parser.add_argument('--web-listen', type=str, default='', help='start webserver to serve results, example: "127.0.0.1:8080"') + parser.add_argument('--markdown', nargs='?', default=False, const=True, help='show markdown output') + cfg = parser.parse_args() + print('Config:') + print('\n'.join(f' {field} = {getattr(cfg, field)}' for field in vars(cfg)), '\n') + + if cfg.web_listen != '': + from aiohttp import web + + results = [process_dataset(d, cfg.providers, cfg.results_dir) for d in cfg.datasets] + + if cfg.markdown: + markdown(cfg.providers, results) + else: + show(cfg.providers, results) + + if cfg.web_listen != '': + host, port = cfg.web_listen.rsplit(':') + serve_web(host, int(port), cfg.providers, results) diff --git a/benchmark/datasets/largest1k.tar.gz b/benchmark/datasets/largest1k.tar.gz new file mode 100644 index 0000000..0f9806d --- /dev/null +++ b/benchmark/datasets/largest1k.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a24efc6b81a8ed3fc9776de18e5215f6a7c618df7e3015ee967ee44b469a961f +size 7076010 diff --git a/benchmark/datasets/random50k.tar.gz b/benchmark/datasets/random50k.tar.gz new file mode 100644 index 0000000..e9d24f5 --- /dev/null +++ b/benchmark/datasets/random50k.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68a2516bb70df861a2b0ebd0c750d50843f3815602a3174822cf0fec8ac56e59 +size 200248543 diff --git a/benchmark/datasets/vyper.tar.gz b/benchmark/datasets/vyper.tar.gz new file mode 100644 index 0000000..2bda2b3 --- /dev/null +++ b/benchmark/datasets/vyper.tar.gz @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:924540a4579829389e423f1f3f151d5db9748b68f295dac350a508319840d8a5 +size 2985881 diff --git a/benchmark/download.py b/benchmark/download.py new file mode 100644 index 0000000..163a7c7 --- /dev/null +++ b/benchmark/download.py @@ -0,0 +1,98 @@ +import argparse +import json +import re +import os + +import aiohttp +import asyncio + +saved_cnt = 0 + +async def worker(cfg, q: asyncio.Queue): + global saved_cnt + async with aiohttp.ClientSession() as session: + while True: + addr = await q.get() + if addr is None: + break + out_path = f'{cfg.out_dir}/{addr}.json' + + print(f'Begin {addr} process') + if os.path.isfile(out_path): + print(f'Skip {addr}: already exists') + saved_cnt += 1 + continue + + if cfg.limit != 0 and saved_cnt >= cfg.limit: + print(f'Skip {addr}: limit reached') + continue + + req = {'method':'eth_getCode','params':[addr, 'latest'],'id':1,'jsonrpc':'2.0'} + async with session.post(cfg.rpc_url, json=req) as r: + code = (await r.json())['result'] + if not cfg.code_regexp.match(code): + print(f'Skip {addr}: regexp not matched') + continue + + u = f'https://api.etherscan.io/api?module=contract&action=getabi&apikey={cfg.etherscan_api_key}&address={addr}' + async with session.get(u) as r: + abi = (await r.json())['result'] + if abi == 'Contract source code not verified': + print(f'Skip {addr}: {abi}') + await asyncio.sleep(cfg.threads * 0.3) # dirty hack: limit ~3 rps for etherscan + continue + assert abi.startswith('['), abi + abi = json.loads(abi) + + await asyncio.sleep(cfg.threads * 0.3) # dirty hack: limit ~3 rps for etherscan + + if cfg.limit != 0 and saved_cnt >= cfg.limit: + print(f'Skip {addr}: limit reached') + continue + + with open(out_path, 'w') as fh: + json.dump({'code': code, 'abi': abi}, fh) + saved_cnt += 1 + print(f'Saved {addr}, {saved_cnt}th address') + + +async def reader(cfg, q: asyncio.Queue): + with open(cfg.addrs_list, 'r') as fh: + for line in fh: + if cfg.limit != 0 and saved_cnt >= cfg.limit: + break + addr = line.rstrip() + if not addr.startswith('0x'): + addr = f'0x{addr}' + await q.put(addr) + + for _ in range(cfg.threads): + await q.put(None) + + +async def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--addrs-list', type=str, required=True, help='file with contract addresses') + parser.add_argument('--out-dir', type=str, required=True, help='output directory') + parser.add_argument('--rpc-url', type=str, default='http://127.0.0.1:8545', help='rpc url of ethereum node') + parser.add_argument('--etherscan-api-key', type=str, required=True, help='etherscan.io api key') + parser.add_argument('--code-regexp', default='', help='code regexp', type=re.compile) + parser.add_argument('--limit', type=int, required=False, default=0, help='limit') + parser.add_argument('--threads', type=int, required=False, default=2, help='threads') + cfg = parser.parse_args() + print('Config:') + print('\n'.join(f' {field} = {getattr(cfg, field)}' for field in vars(cfg)), '\n') + + q = asyncio.Queue(maxsize = cfg.threads) + await asyncio.gather( + reader(cfg, q), + *[worker(cfg, q) for _ in range(cfg.threads)] + ) + print(f'Finished, got {saved_cnt} contracts') + + +if __name__ == '__main__': + try: + asyncio.run(main()) + except KeyboardInterrupt: + pass diff --git a/benchmark/index.html b/benchmark/index.html new file mode 100644 index 0000000..65471a2 --- /dev/null +++ b/benchmark/index.html @@ -0,0 +1,141 @@ + + + + + + EVMole: Benchmark Results + + + + + + +
+ + + + all + any errors + "{{name}}" errors + + + + + + + {{name}} + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/benchmark/providers/etherscan/Dockerfile b/benchmark/providers/etherscan/Dockerfile new file mode 100644 index 0000000..d38b74a --- /dev/null +++ b/benchmark/providers/etherscan/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.12-slim +WORKDIR /app +RUN pip3 install pycryptodome==3.19 +COPY main.py /app +ENTRYPOINT ["python3", "/app/main.py"] diff --git a/benchmark/providers/etherscan/main.py b/benchmark/providers/etherscan/main.py new file mode 100644 index 0000000..15612be --- /dev/null +++ b/benchmark/providers/etherscan/main.py @@ -0,0 +1,46 @@ +import json +import os +import sys + +from Crypto.Hash import keccak + +def sign(inp: bytes) -> str: + return keccak.new(digest_bits=256, data=inp).digest()[:4].hex() + +def join_inputs(inputs) -> str: + if len(inputs) == 0: + return '' + n = '' + for v in inputs: + if v['type'].startswith('tuple'): + n += '(' + join_inputs(v['components']) + ')' + v['type'][5:] + else: + n += v['type'] + n += ',' + return n[:-1] + +def process(abi) -> list[str]: + ret = {} + for x in abi: + if x['type'] != 'function': + continue + n = x['name'] + '(' + join_inputs(x['inputs']) + ')' + sg = sign(n.encode('ascii')) + ret[sg] = n + return list(ret.keys()) + +if len(sys.argv) != 3: + print('Usage: python3 main.py INPUT_DIR OUTPUT_FILE') + sys.exit(1) + + +ret = {} +indir = sys.argv[1] +outfile = sys.argv[2] +for fname in os.listdir(indir): + with open(f'{indir}/{fname}', 'r') as fh: + d = json.load(fh) + ret[fname] = process(d['abi']) + +with open(outfile, 'w') as fh: + json.dump(ret, fh) diff --git a/benchmark/providers/evmole-js/Dockerfile b/benchmark/providers/evmole-js/Dockerfile new file mode 100644 index 0000000..472aece --- /dev/null +++ b/benchmark/providers/evmole-js/Dockerfile @@ -0,0 +1,5 @@ +FROM node:21 +WORKDIR /app +COPY main.mjs /app +COPY js /app/js +ENTRYPOINT ["node", "/app/main.mjs"] diff --git a/benchmark/providers/evmole-js/main.mjs b/benchmark/providers/evmole-js/main.mjs new file mode 100644 index 0000000..c822fea --- /dev/null +++ b/benchmark/providers/evmole-js/main.mjs @@ -0,0 +1,22 @@ +import {readdirSync, readFileSync, writeFileSync} from 'fs' + +import {functionSelectors} from './js/src/index.js' + +const argv = process.argv; +if (argv.length != 4) { + console.log('Usage: node main.js INPUT_DIR OUTPUT_FILE') + process.exit(1) +} + +const indir = argv[2]; +const outfile = argv[3]; + +const res = Object.fromEntries( + readdirSync(indir).map( + file => [ + file, + functionSelectors(JSON.parse(readFileSync(`${indir}/${file}`))['code']) + ] + ) +); +writeFileSync(outfile, JSON.stringify(res), 'utf8'); diff --git a/benchmark/providers/evmole-py/Dockerfile b/benchmark/providers/evmole-py/Dockerfile new file mode 100644 index 0000000..b9fb350 --- /dev/null +++ b/benchmark/providers/evmole-py/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.12-slim +WORKDIR /app +COPY main.py /app +COPY evmole /app/evmole +ENTRYPOINT ["python3", "/app/main.py"] diff --git a/benchmark/providers/evmole-py/main.py b/benchmark/providers/evmole-py/main.py new file mode 100644 index 0000000..6a2bf0c --- /dev/null +++ b/benchmark/providers/evmole-py/main.py @@ -0,0 +1,21 @@ +import json +import os +import sys + +from evmole import function_selectors + +if len(sys.argv) != 3: + print('Usage: python3 main.py INPUT_DIR OUTPUT_FILE') + sys.exit(1) + + +ret = {} +indir = sys.argv[1] +outfile = sys.argv[2] +for fname in os.listdir(indir): + with open(f'{indir}/{fname}', 'r') as fh: + d = json.load(fh) + ret[fname] = function_selectors(bytes.fromhex(d['code'][2:])) + +with open(outfile, 'w') as fh: + json.dump(ret, fh) diff --git a/benchmark/providers/simple/Dockerfile b/benchmark/providers/simple/Dockerfile new file mode 100644 index 0000000..2834737 --- /dev/null +++ b/benchmark/providers/simple/Dockerfile @@ -0,0 +1,4 @@ +FROM python:3.12-slim +WORKDIR /app +COPY main.py /app +ENTRYPOINT ["python3", "/app/main.py"] diff --git a/benchmark/providers/simple/main.py b/benchmark/providers/simple/main.py new file mode 100644 index 0000000..4a89224 --- /dev/null +++ b/benchmark/providers/simple/main.py @@ -0,0 +1,33 @@ +import json +import os +import sys + +def process(code: bytes) -> list[str]: + ret = [] + for i in range(len(code) - 5): + # PUSH2/PUSH3 + if (code[i] == 0x62 or code[i] == 0x63): + off = code[i] - 0x62 + + # EQ or (DUP2 + EQ) + if (code[i+off+4] == 0x14) or (code[i+off+4] == 0x81 and code[i+off+5] == 0x14): + ret.append(code[i+1:i+4+off]) + + return [s.hex().zfill(8) for s in ret] + + +if len(sys.argv) != 3: + print('Usage: python3 main.py INPUT_DIR OUTPUT_FILE') + sys.exit(1) + + +ret = {} +indir = sys.argv[1] +outfile = sys.argv[2] +for fname in os.listdir(indir): + with open(f'{indir}/{fname}', 'r') as fh: + d = json.load(fh) + ret[fname] = process(bytes.fromhex(d['code'][2:])) + +with open(outfile, 'w') as fh: + json.dump(ret, fh) diff --git a/benchmark/providers/whatsabi/Dockerfile b/benchmark/providers/whatsabi/Dockerfile new file mode 100644 index 0000000..1b54e4b --- /dev/null +++ b/benchmark/providers/whatsabi/Dockerfile @@ -0,0 +1,5 @@ +FROM node:21 +WORKDIR /app +RUN npm install @shazow/whatsabi@0.9.1 +COPY main.mjs /app +ENTRYPOINT ["node", "/app/main.mjs"] diff --git a/benchmark/providers/whatsabi/main.mjs b/benchmark/providers/whatsabi/main.mjs new file mode 100644 index 0000000..cd0e130 --- /dev/null +++ b/benchmark/providers/whatsabi/main.mjs @@ -0,0 +1,22 @@ +import {readdirSync, readFileSync, writeFileSync} from 'fs' + +import { whatsabi } from "@shazow/whatsabi"; + +const argv = process.argv; +if (argv.length != 4) { + console.log('Usage: node main.js INPUT_DIR OUTPUT_FILE') + process.exit(1) +} + +const indir = argv[2]; +const outfile = argv[3]; + +const res = Object.fromEntries( + readdirSync(indir).map( + file => [ + file, + whatsabi.selectorsFromBytecode(JSON.parse(readFileSync(`${indir}/${file}`))['code']).map(x => x.slice(2)) // remove '0x' prefix + ] + ) +); +writeFileSync(outfile, JSON.stringify(res), 'utf8'); diff --git a/benchmark/results/.gitkeep b/benchmark/results/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/evmole/README.md b/evmole/README.md new file mode 100644 index 0000000..ff49744 --- /dev/null +++ b/evmole/README.md @@ -0,0 +1,3 @@ +# Python implementation + +See [examples/python.py](../examples/python.py) and [main README.md](../README.md#python) diff --git a/evmole/__init__.py b/evmole/__init__.py new file mode 100644 index 0000000..6a5da6c --- /dev/null +++ b/evmole/__init__.py @@ -0,0 +1 @@ +from .selectors import function_selectors diff --git a/evmole/evm/__init__.py b/evmole/evm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/evmole/evm/memory.py b/evmole/evm/memory.py new file mode 100644 index 0000000..d749c27 --- /dev/null +++ b/evmole/evm/memory.py @@ -0,0 +1,36 @@ +class Memory: + def __init__(self): + self._data = [] + + def __str__(self): + r = f'{len(self._data)} elems:\n' + return r + '\n'.join(f' - {off}: {val.hex()} | {type(val)}' for off, val in self._data) + + def store(self, offset: int, value: bytes): + self._data.append((offset, value)) + + def load(self, offset: int) -> tuple[bytes, list[bytes]]: + self._data = sorted(self._data) + ret = b'' + used = [] + for off, val in self._data: + b = off + len(val) + if b <= offset: + continue + if offset + (32 - len(ret)) <= off: + break + + if off > offset: + ret += b'\x00' * (off - offset) + val + elif off < offset: + ret += val[offset - off :] + else: + ret += val + used.append(val) + offset += b + + if len(ret) > 32: + ret = ret[:32] + else: + ret += b'\x00' * max(0, 32 - len(ret)) + return ret, used diff --git a/evmole/evm/opcodes.py b/evmole/evm/opcodes.py new file mode 100644 index 0000000..7f341e2 --- /dev/null +++ b/evmole/evm/opcodes.py @@ -0,0 +1,172 @@ +from enum import Enum + + +class Op(int, Enum): + def __new__(cls, code, *args, **kwds): + obj = int.__new__(cls, code) + obj._value_ = code + return obj + + def __init__(self, code, stack_in=0, stack_out=0, blen=1, gas=None) -> None: + self.code = code + self.stack_in = stack_in + self.stack_out = stack_out + self.blen = blen + self.gas: int | None = gas + + STOP = (0x00, 0, 0, 1, 0) + ADD = (0x01, 2, 1, 1, 3) + MUL = (0x02, 2, 1, 1, 5) + SUB = (0x03, 2, 1, 1, 3) + DIV = (0x04, 2, 1, 1, 5) + SDIV = (0x05, 2, 1, 1, 5) + MOD = (0x06, 2, 1, 1, 5) + SMOD = (0x07, 2, 1, 1, 5) + ADDMOD = (0x08, 3, 1, 1, 8) + MULMOD = (0x09, 3, 1, 1, 8) + EXP = (0x0A, 2, 1, 1, None) + SIGNEXTEND = (0x0B, 2, 1, 1, 5) + + LT = (0x10, 2, 1, 1, 3) + GT = (0x11, 2, 1, 1, 3) + SLT = (0x12, 2, 1, 1, 3) + SGT = (0x13, 2, 1, 1, 3) + EQ = (0x14, 2, 1, 1, 3) + ISZERO = (0x15, 1, 1, 1, 3) + AND = (0x16, 2, 1, 1, 3) + OR = (0x17, 2, 1, 1, 3) + XOR = (0x18, 2, 1, 1, 3) + NOT = (0x19, 1, 1, 1, 3) + BYTE = (0x1A, 2, 1, 1, 3) + SHL = (0x1B, 2, 1, 1, 3) + SHR = (0x1C, 2, 1, 1, 3) + SAR = (0x1D, 2, 1, 1, 3) + + KECCAK256 = (0x20, 2, 1, 1, None) + + ADDRESS = (0x30, 0, 1, 1, 2) + BALANCE = (0x31, 1, 1, 1, None) + ORIGIN = (0x32, 0, 1, 1, 2) + CALLER = (0x33, 0, 1, 1, 2) + CALLVALUE = (0x34, 0, 1, 1, 2) + CALLDATALOAD = (0x35, 1, 1, 1, 3) + CALLDATASIZE = (0x36, 0, 1, 1, 2) + CALLDATACOPY = (0x37, 3, 0, 1, None) + CODESIZE = (0x38, 0, 1, 1, 2) + CODECOPY = (0x39, 3, 0, 1, None) + GASPRICE = (0x3A, 0, 1, 1, 2) + EXTCODESIZE = (0x3B, 1, 1, 1, None) + EXTCODECOPY = (0x3C, 4, 0, 1, None) + RETURNDATASIZE = (0x3D, 0, 1, 1, None) + RETURNDATACOPY = (0x3E, 3, 0, 1, None) + EXTCODEHASH = (0x3F, 1, 1, 1, None) + + BLOCKHASH = (0x40, 1, 1, 1, 20) + COINBASE = (0x41, 0, 1, 1, 2) + TIMESTAMP = (0x42, 0, 1, 1, 2) + NUMBER = (0x43, 0, 1, 1, 2) + DIFFICULTY = (0x44, 0, 1, 1, 2) + GASLIMIT = (0x45, 0, 1, 1, 2) + CHAINID = (0x46, 0, 1, 1, 2) + SELFBALANCE = (0x47, 0, 1, 1, 5) + BASEFEE = (0x48, 0, 1, 1, 2) + + POP = (0x50, 1, 0, 1, 2) + MLOAD = (0x51, 1, 1, 1, None) + MSTORE = (0x52, 2, 0, 1, None) + MSTORE8 = (0x53, 2, 0, 1, None) + SLOAD = (0x54, 1, 1, 1, None) + SSTORE = (0x55, 2, 0, 1, None) + JUMP = (0x56, 1, 0, 1, 8) + JUMPI = (0x57, 2, 0, 1, 10) + PC = (0x58, 0, 1, 1, 2) + MSIZE = (0x59, 0, 1, 1, 2) + GAS = (0x5A, 0, 1, 1, 2) + JUMPDEST = (0x5B, 0, 0, 1, 1) + PUSH0 = (0x5F, 0, 1, 1, 2) + + PUSH1 = (0x60, 0, 1, 2, 3) + PUSH2 = (0x61, 0, 1, 3, 3) + PUSH3 = (0x62, 0, 1, 4, 3) + PUSH4 = (0x63, 0, 1, 5, 3) + PUSH5 = (0x64, 0, 1, 6, 3) + PUSH6 = (0x65, 0, 1, 7, 3) + PUSH7 = (0x66, 0, 1, 8, 3) + PUSH8 = (0x67, 0, 1, 9, 3) + PUSH9 = (0x68, 0, 1, 10, 3) + PUSH10 = (0x69, 0, 1, 11, 3) + PUSH11 = (0x6A, 0, 1, 12, 3) + PUSH12 = (0x6B, 0, 1, 13, 3) + PUSH13 = (0x6C, 0, 1, 14, 3) + PUSH14 = (0x6D, 0, 1, 15, 3) + PUSH15 = (0x6E, 0, 1, 16, 3) + PUSH16 = (0x6F, 0, 1, 17, 3) + + PUSH17 = (0x70, 0, 1, 18, 3) + PUSH18 = (0x71, 0, 1, 19, 3) + PUSH19 = (0x72, 0, 1, 20, 3) + PUSH20 = (0x73, 0, 1, 21, 3) + PUSH21 = (0x74, 0, 1, 22, 3) + PUSH22 = (0x75, 0, 1, 23, 3) + PUSH23 = (0x76, 0, 1, 24, 3) + PUSH24 = (0x77, 0, 1, 25, 3) + PUSH25 = (0x78, 0, 1, 26, 3) + PUSH26 = (0x79, 0, 1, 27, 3) + PUSH27 = (0x7A, 0, 1, 28, 3) + PUSH28 = (0x7B, 0, 1, 29, 3) + PUSH29 = (0x7C, 0, 1, 30, 3) + PUSH30 = (0x7D, 0, 1, 31, 3) + PUSH31 = (0x7E, 0, 1, 32, 3) + PUSH32 = (0x7F, 0, 1, 33, 3) + + DUP1 = (0x80, 1, 2, 1, 3) + DUP2 = (0x81, 2, 3, 1, 3) + DUP3 = (0x82, 3, 4, 1, 3) + DUP4 = (0x83, 4, 5, 1, 3) + DUP5 = (0x84, 5, 6, 1, 3) + DUP6 = (0x85, 6, 7, 1, 3) + DUP7 = (0x86, 7, 8, 1, 3) + DUP8 = (0x87, 8, 9, 1, 3) + DUP9 = (0x88, 9, 10, 1, 3) + DUP10 = (0x89, 10, 11, 1, 3) + DUP11 = (0x8A, 11, 12, 1, 3) + DUP12 = (0x8B, 12, 13, 1, 3) + DUP13 = (0x8C, 13, 14, 1, 3) + DUP14 = (0x8D, 14, 15, 1, 3) + DUP15 = (0x8E, 15, 16, 1, 3) + DUP16 = (0x8F, 16, 17, 1, 3) + + SWAP1 = (0x90, 2, 2, 1, 3) + SWAP2 = (0x91, 3, 3, 1, 3) + SWAP3 = (0x92, 4, 4, 1, 3) + SWAP4 = (0x93, 5, 5, 1, 3) + SWAP5 = (0x94, 6, 6, 1, 3) + SWAP6 = (0x95, 7, 7, 1, 3) + SWAP7 = (0x96, 8, 8, 1, 3) + SWAP8 = (0x97, 9, 9, 1, 3) + SWAP9 = (0x98, 10, 10, 1, 3) + SWAP10 = (0x99, 11, 11, 1, 3) + SWAP11 = (0x9A, 12, 12, 1, 3) + SWAP12 = (0x9B, 13, 13, 1, 3) + SWAP13 = (0x9C, 14, 14, 1, 3) + SWAP14 = (0x9D, 15, 15, 1, 3) + SWAP15 = (0x9E, 16, 16, 1, 3) + SWAP16 = (0x9F, 17, 17, 1, 3) + + LOG0 = (0xA0, 2, 0, 1, None) + LOG1 = (0xA1, 3, 0, 1, None) + LOG2 = (0xA2, 4, 0, 1, None) + LOG3 = (0xA3, 5, 0, 1, None) + LOG4 = (0xA4, 6, 0, 1, None) + + CREATE = (0xF0, 3, 1, 1, None) + CALL = (0xF1, 7, 1, 1, None) + CALLCODE = (0xF2, 7, 1, 1, None) + RETURN = (0xF3, 2, 0, 1, None) + DELEGATECALL = (0xF4, 6, 1, 1, None) + CREATE2 = (0xF5, 4, 1, 1, None) + + STATICCALL = (0xFA, 6, 1, 1, None) + REVERT = (0xFD, 2, 0, 1, None) + INVALID = (0xFE, 0, 0, 1, None) + SELFDESTRUCT = (0xFF, 1, 0, 1, None) diff --git a/evmole/evm/stack.py b/evmole/evm/stack.py new file mode 100644 index 0000000..4b72cad --- /dev/null +++ b/evmole/evm/stack.py @@ -0,0 +1,31 @@ +class Stack: + def __init__(self): + self._data = [] + + def __str__(self): + r = f'{len(self._data)} elems:' + return r + ('\n' if len(self._data) else '') + '\n'.join(f' - {el.hex()} | {len(el)} | {type(el)}' for el in self._data) + + def push(self, val: bytes): + self._data.append(val) + + def pop(self) -> bytes: + return self._data.pop() + + def peek(self, n: int = 0) -> bytes | None: + if len(self._data) < n + 1: + return None + return self._data[-1 * (n + 1)] + + def dup(self, n: int): + self.push(self._data[-n]) + + def swap(self, n: int): + self._data[-1], self._data[-n - 1] = self._data[-n - 1], self._data[-1] + + def push_uint(self, val: int): + bl = val.bit_length() if val != 0 else 1 + self.push(val.to_bytes(bl, byteorder='big', signed=False)) + + def pop_uint(self) -> int: + return int.from_bytes(self.pop(), 'big', signed=False) diff --git a/evmole/evm/vm.py b/evmole/evm/vm.py new file mode 100644 index 0000000..65e0030 --- /dev/null +++ b/evmole/evm/vm.py @@ -0,0 +1,169 @@ +from typing import Any + +from .opcodes import Op +from .stack import Stack +from .memory import Memory + +E256 = 2**256 +E256M1 = E256 - 1 + + +class Vm: + def __init__(self, *, code: bytes, calldata): + self.code = code + self.pc = 0 + self.stack = Stack() + self.memory = Memory() + self.stopped = False + self.calldata = calldata + + def __str__(self): + return '\n'.join( + ( + f'Vm ({id(self):x}):', + f' .pc = {self.pc} | {self.current_op()}', + f' .stack = {self.stack}', + f' .memory = {self.memory}', + ) + ) + + def __copy__(self): + obj = Vm(code=self.code, calldata=self.calldata) + obj.pc = self.pc + obj.memory._data = self.memory._data[:] + obj.stack._data = self.stack._data[:] + obj.stopped = self.stopped + return obj + + def current_op(self) -> Op: + return Op(self.code[self.pc]) + + def step(self) -> tuple[Op, int, *tuple[Any, ...]]: + ret = self._exec_next_opcode() + op, gas_used = ret[0], ret[1] + assert gas_used != -1, f'Op {op} with unset gas_used' + + if op not in {Op.JUMP, Op.JUMPI}: + self.pc += op.blen + + if self.pc >= len(self.code): + self.stopped = True + return ret + + def _exec_next_opcode(self) -> tuple[Op, int, *tuple[Any, ...]]: + op = self.current_op() + gas_used = op.gas if op.gas is not None else -1 + match op: + case op if op >= Op.PUSH0 and op <= Op.PUSH32: + n = op - Op.PUSH0 + args = self.code[(self.pc + 1) : (self.pc + 1 + n)] if n != 0 else b'\x00' + self.stack.push(args) + return (op, gas_used) + + case op if op in {Op.JUMP, Op.JUMPI}: + s0 = self.stack.pop_uint() + assert self.code[s0] == Op.JUMPDEST.code, 'not JUMPDEST, pos %d op %02x' % (s0, self.code[s0]) + if op == Op.JUMPI: + s1 = self.stack.pop_uint() + if s1 == 0: + self.pc += 1 + return (op, gas_used) + self.pc = s0 + return (op, gas_used) + + case op if op >= Op.DUP1 and op <= Op.DUP16: + self.stack.dup(op - Op.DUP1 + 1) + return (op, gas_used) + + case Op.JUMPDEST: + return (op, gas_used) + + case Op.REVERT: + self.stack.pop() + self.stack.pop() + self.stopped = True + return (op, 4) + + case Op.ISZERO: + raws0 = self.stack.pop() + s0 = int.from_bytes(raws0, 'big', signed=False) + res = 0 if s0 else 1 + self.stack.push_uint(res) + return (op, gas_used, raws0) + + case Op.POP: + self.stack.pop() + return (op, gas_used) + + case op if op in {Op.LT, Op.GT, Op.EQ, Op.SUB, Op.DIV, Op.EXP, Op.XOR, Op.AND, Op.SHR}: + raws0 = self.stack.pop() + raws1 = self.stack.pop() + + s0 = int.from_bytes(raws0, 'big', signed=False) + s1 = int.from_bytes(raws1, 'big', signed=False) + + match op: + case Op.EQ: + res = 1 if s0 == s1 else 0 + case Op.GT: + res = 1 if s0 > s1 else 0 + case Op.LT: + res = 1 if s0 < s1 else 0 + case Op.SUB: + res = (s0 - s1) & E256M1 + case Op.DIV: + res = 0 if s1 == 0 else s0 // s1 + case Op.EXP: + res = pow(s0, s1, E256) + gas_used = 50 * (1 + (s1.bit_length() // 8)) # ~approx + case Op.XOR: + res = s0 ^ s1 + case Op.AND: + res = s0 & s1 + case Op.SHR: + res = 0 if s0 >= 256 else (s1 >> s0) & E256M1 + case _: + raise Exception(f'BUG: op {op} not handled in match') + + self.stack.push_uint(res) + return (op, gas_used, raws0, raws1) + + case Op.CALLVALUE: + self.stack.push_uint(0) # msg.value == 0 + return (op, gas_used) + + case Op.CALLDATALOAD: + offset = self.stack.pop_uint() + self.stack.push(self.calldata.load(offset)) + return (op, gas_used) + + case Op.CALLDATASIZE: + self.stack.push_uint(len(self.calldata)) + return (op, gas_used) + + case op if op >= Op.SWAP1 and op <= Op.SWAP16: + self.stack.swap(op - Op.SWAP1 + 1) + return (op, gas_used) + + case Op.MSTORE: + offset = self.stack.pop_uint() + value = self.stack.pop() + self.memory.store(offset, value) + return (op, 3) + + case Op.MLOAD: + offset = self.stack.pop_uint() + val, used = self.memory.load(offset) + self.stack.push(val) + return (op, 4, used) + + case Op.CALLDATACOPY: + mem_off = self.stack.pop_uint() + src_off = self.stack.pop_uint() + size = self.stack.pop_uint() + value = self.calldata.load(src_off, size) + self.memory.store(mem_off, value) + return (op, 4) + + case _: + raise Exception(f'unknown op {op}') diff --git a/evmole/selectors.py b/evmole/selectors.py new file mode 100644 index 0000000..d038061 --- /dev/null +++ b/evmole/selectors.py @@ -0,0 +1,92 @@ +import copy + +from .evm.vm import Vm +from .evm.opcodes import Op + + +class CallData(bytes): + def load(self, offset: int, size: int = 32): + val = self[offset : min(offset + size, len(self))] + return CallData(val + b'\x00' * max(0, size - len(val))) + + +class CallDataSignature(bytes): + pass + + +def process(vm: Vm, gas_limit: int) -> tuple[list[bytes], int]: + selectors = [] + gas_used = 0 + while not vm.stopped: + # print(vm, '\n') + try: + ret = vm.step() + gas_used += ret[1] + if gas_used > gas_limit: + raise Exception(f'gas overflow: {gas_used} > {gas_limit}') + except Exception as ex: + _ = ex + # raise ex + break + + match ret: + # fmt: off + case ((Op.XOR | Op.EQ as op, _, bytes() as s1, CallDataSignature())) | \ + ((Op.XOR | Op.EQ as op, _, CallDataSignature(), bytes() as s1) + ): + #fmt: on + selectors.append(s1[-4:]) + vm.stack.pop() + vm.stack.push_uint(1 if op == Op.XOR else 0) + + # fmt: off + case (Op.SUB, _, CallDataSignature(), bytes() as s1) | \ + (Op.SUB, _, bytes() as s1, CallDataSignature() + ): + #fmt: on + selectors.append(s1[-4:]) + + case (Op.LT | Op.GT, *_): + cloned_vm = copy.copy(vm) + s, g = process(cloned_vm, gas_limit // 2) + selectors += s + gas_used += g + v = vm.stack.pop_uint() + vm.stack.push_uint(1 if v == 0 else 0) + + # fmt: off + case (Op.SHR, _, _, CallDataSignature() | CallData()) | \ + (Op.AND, _, CallDataSignature() | CallData(), _) | \ + (Op.AND, _, _, CallDataSignature() | CallData()) | \ + (Op.DIV, _, CallDataSignature() | CallData(), _ + ): + # fmt: on + v = vm.stack.peek() + assert v is not None + if v[-4:] == vm.calldata[:4]: + v = vm.stack.pop() + vm.stack.push(CallDataSignature(v)) + + case (Op.ISZERO, _, CallDataSignature()): + selectors.append(b'\x00\x00\x00\x00') + + case (Op.MLOAD, _, list() as used): + for u in used: + if isinstance(u, CallData): + p = vm.stack.peek() + if p is not None and p[-4:] == vm.calldata[:4]: + v = vm.stack.pop() + vm.stack.push(CallDataSignature(v)) + break + + return selectors, gas_used + + +def function_selectors(code: bytes | str, gas_limit: int = int(1e6)) -> list[str]: + if isinstance(code, str): + code = bytes.fromhex(code[2:] if code.startswith('0x') else code) + else: + assert isinstance(code, bytes), '`code` arg must be hex-string or bytes' + vm = Vm(code=code, calldata=CallData(b'\xaa\xbb\xcc\xdd')) + selectors, _ = process(vm, gas_limit) + return [s.hex().zfill(8) for s in selectors] diff --git a/examples/example.sol b/examples/example.sol new file mode 100644 index 0000000..49f3fad --- /dev/null +++ b/examples/example.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +contract Example { + uint256 public balance; + + function deposit() public payable { + balance += msg.value; + } +} diff --git a/examples/javascript.mjs b/examples/javascript.mjs new file mode 100644 index 0000000..d66583e --- /dev/null +++ b/examples/javascript.mjs @@ -0,0 +1,14 @@ +import {functionSelectors} from 'evmole' + +// output of `solc example.sol --bin-runtime --optimize` +const code = '0x6080604052600436106025575f3560e01c8063b69ef8a8146029578063d0e30db014604d575b5f80fd5b3480156033575f80fd5b50603b5f5481565b60405190815260200160405180910390f35b60536055565b005b345f8082825460639190606a565b9091555050565b80820180821115608857634e487b7160e01b5f52601160045260245ffd5b9291505056fea2646970667358221220354240f63068d555e9b817619001b0dff6ea630d137edc1a640dae8e3ebb959864736f6c63430008170033' + +let r; +r = functionSelectors(code) +console.log('all signatures with default gas_limit', r) + +r = functionSelectors(code, 100) +console.log('only 1 signature found with so low gas limit', r) + +r = functionSelectors(code, 200) +console.log('200 gas is enough for all 2 signatures', r) diff --git a/examples/python.py b/examples/python.py new file mode 100644 index 0000000..e0e52a5 --- /dev/null +++ b/examples/python.py @@ -0,0 +1,13 @@ +from evmole import function_selectors + +# output of `solc example.sol --bin-runtime --optimize` +code = '0x6080604052600436106025575f3560e01c8063b69ef8a8146029578063d0e30db014604d575b5f80fd5b3480156033575f80fd5b50603b5f5481565b60405190815260200160405180910390f35b60536055565b005b345f8082825460639190606a565b9091555050565b80820180821115608857634e487b7160e01b5f52601160045260245ffd5b9291505056fea2646970667358221220354240f63068d555e9b817619001b0dff6ea630d137edc1a640dae8e3ebb959864736f6c63430008170033' + +r = function_selectors(code) +print('all signatures with default gas_limit', r) + +r = function_selectors(code, gas_limit=100) +print('only 1 signature found with so low gas limit', r) + +r = function_selectors(code, gas_limit=200) +print('200 gas is enough for all 2 signatures', r) diff --git a/js/.eslintrc.cjs b/js/.eslintrc.cjs new file mode 100644 index 0000000..9a8e93f --- /dev/null +++ b/js/.eslintrc.cjs @@ -0,0 +1,27 @@ +module.exports = { + "env": { + "browser": true, + "es2021": true, + "node": true + }, + "extends": "eslint:recommended", + "overrides": [ + { + "env": { + "node": true + }, + "files": [ + ".eslintrc.{js,cjs}" + ], + "parserOptions": { + "sourceType": "script" + } + } + ], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "rules": { + } +} diff --git a/js/.prettierrc b/js/.prettierrc new file mode 100644 index 0000000..e63fa60 --- /dev/null +++ b/js/.prettierrc @@ -0,0 +1,5 @@ +{ + "trailingComma": "all", + "semi": false, + "singleQuote": true +} diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..7e624f0 --- /dev/null +++ b/js/README.md @@ -0,0 +1,3 @@ +# JavaScript implementation + +See [examples/javascript.mjs](../examples/javascript.mjs) and [main README.md](../README.md#javascript) diff --git a/js/package-lock.json b/js/package-lock.json new file mode 100644 index 0000000..4b43f98 --- /dev/null +++ b/js/package-lock.json @@ -0,0 +1,1541 @@ +{ + "name": "evmole", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "evmole", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "esbuild": "0.19.6", + "eslint": "^8.53.0", + "prettier": "3.1.0" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.6.tgz", + "integrity": "sha512-muPzBqXJKCbMYoNbb1JpZh/ynl0xS6/+pLjrofcR3Nad82SbsCogYzUE6Aq9QT3cLP0jR/IVK/NHC9b90mSHtg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.6.tgz", + "integrity": "sha512-KQ/hbe9SJvIJ4sR+2PcZ41IBV+LPJyYp6V1K1P1xcMRup9iYsBoQn4MzE3mhMLOld27Au2eDcLlIREeKGUXpHQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.6.tgz", + "integrity": "sha512-VVJVZQ7p5BBOKoNxd0Ly3xUM78Y4DyOoFKdkdAe2m11jbh0LEU4bPles4e/72EMl4tapko8o915UalN/5zhspg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.6.tgz", + "integrity": "sha512-91LoRp/uZAKx6ESNspL3I46ypwzdqyDLXZH7x2QYCLgtnaU08+AXEbabY2yExIz03/am0DivsTtbdxzGejfXpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.6.tgz", + "integrity": "sha512-QCGHw770ubjBU1J3ZkFJh671MFajGTYMZumPs9E/rqU52md6lIil97BR0CbPq6U+vTh3xnTNDHKRdR8ggHnmxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.6.tgz", + "integrity": "sha512-J53d0jGsDcLzWk9d9SPmlyF+wzVxjXpOH7jVW5ae7PvrDst4kiAz6sX+E8btz0GB6oH12zC+aHRD945jdjF2Vg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.6.tgz", + "integrity": "sha512-hn9qvkjHSIB5Z9JgCCjED6YYVGCNpqB7dEGavBdG6EjBD8S/UcNUIlGcB35NCkMETkdYwfZSvD9VoDJX6VeUVA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.6.tgz", + "integrity": "sha512-G8IR5zFgpXad/Zp7gr7ZyTKyqZuThU6z1JjmRyN1vSF8j0bOlGzUwFSMTbctLAdd7QHpeyu0cRiuKrqK1ZTwvQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.6.tgz", + "integrity": "sha512-HQCOrk9XlH3KngASLaBfHpcoYEGUt829A9MyxaI8RMkfRA8SakG6YQEITAuwmtzFdEu5GU4eyhKcpv27dFaOBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.6.tgz", + "integrity": "sha512-22eOR08zL/OXkmEhxOfshfOGo8P69k8oKHkwkDrUlcB12S/sw/+COM4PhAPT0cAYW/gpqY2uXp3TpjQVJitz7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.6.tgz", + "integrity": "sha512-82RvaYAh/SUJyjWA8jDpyZCHQjmEggL//sC7F3VKYcBMumQjUL3C5WDl/tJpEiKtt7XrWmgjaLkrk205zfvwTA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.6.tgz", + "integrity": "sha512-8tvnwyYJpR618vboIv2l8tK2SuK/RqUIGMfMENkeDGo3hsEIrpGldMGYFcWxWeEILe5Fi72zoXLmhZ7PR23oQA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.6.tgz", + "integrity": "sha512-Qt+D7xiPajxVNk5tQiEJwhmarNnLPdjXAoA5uWMpbfStZB0+YU6a3CtbWYSy+sgAsnyx4IGZjWsTzBzrvg/fMA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.6.tgz", + "integrity": "sha512-lxRdk0iJ9CWYDH1Wpnnnc640ajF4RmQ+w6oHFZmAIYu577meE9Ka/DCtpOrwr9McMY11ocbp4jirgGgCi7Ls/g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.6.tgz", + "integrity": "sha512-MopyYV39vnfuykHanRWHGRcRC3AwU7b0QY4TI8ISLfAGfK+tMkXyFuyT1epw/lM0pflQlS53JoD22yN83DHZgA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.6.tgz", + "integrity": "sha512-UWcieaBzsN8WYbzFF5Jq7QULETPcQvlX7KL4xWGIB54OknXJjBO37sPqk7N82WU13JGWvmDzFBi1weVBajPovg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.6.tgz", + "integrity": "sha512-EpWiLX0fzvZn1wxtLxZrEW+oQED9Pwpnh+w4Ffv8ZLuMhUoqR9q9rL4+qHW8F4Mg5oQEKxAoT0G+8JYNqCiR6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.6.tgz", + "integrity": "sha512-fFqTVEktM1PGs2sLKH4M5mhAVEzGpeZJuasAMRnvDZNCV0Cjvm1Hu35moL2vC0DOrAQjNTvj4zWrol/lwQ8Deg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.6.tgz", + "integrity": "sha512-M+XIAnBpaNvaVAhbe3uBXtgWyWynSdlww/JNZws0FlMPSBy+EpatPXNIlKAdtbFVII9OpX91ZfMb17TU3JKTBA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.6.tgz", + "integrity": "sha512-2DchFXn7vp/B6Tc2eKdTsLzE0ygqKkNUhUBCNtMx2Llk4POIVMUq5rUYjdcedFlGLeRe1uLCpVvCmE+G8XYybA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.6.tgz", + "integrity": "sha512-PBo/HPDQllyWdjwAVX+Gl2hH0dfBydL97BAH/grHKC8fubqp02aL4S63otZ25q3sBdINtOBbz1qTZQfXbP4VBg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.6.tgz", + "integrity": "sha512-OE7yIdbDif2kKfrGa+V0vx/B3FJv2L4KnIiLlvtibPyO9UkgO3rzYE0HhpREo2vmJ1Ixq1zwm9/0er+3VOSZJA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", + "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", + "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", + "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", + "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", + "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.19.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.6.tgz", + "integrity": "sha512-Xl7dntjA2OEIvpr9j0DVxxnog2fyTGnyVoQXAMQI6eR3mf9zCQds7VIKUDCotDgE/p4ncTgeRqgX8t5d6oP4Gw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.6", + "@esbuild/android-arm64": "0.19.6", + "@esbuild/android-x64": "0.19.6", + "@esbuild/darwin-arm64": "0.19.6", + "@esbuild/darwin-x64": "0.19.6", + "@esbuild/freebsd-arm64": "0.19.6", + "@esbuild/freebsd-x64": "0.19.6", + "@esbuild/linux-arm": "0.19.6", + "@esbuild/linux-arm64": "0.19.6", + "@esbuild/linux-ia32": "0.19.6", + "@esbuild/linux-loong64": "0.19.6", + "@esbuild/linux-mips64el": "0.19.6", + "@esbuild/linux-ppc64": "0.19.6", + "@esbuild/linux-riscv64": "0.19.6", + "@esbuild/linux-s390x": "0.19.6", + "@esbuild/linux-x64": "0.19.6", + "@esbuild/netbsd-x64": "0.19.6", + "@esbuild/openbsd-x64": "0.19.6", + "@esbuild/sunos-x64": "0.19.6", + "@esbuild/win32-arm64": "0.19.6", + "@esbuild/win32-ia32": "0.19.6", + "@esbuild/win32-x64": "0.19.6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.3", + "@eslint/js": "8.54.0", + "@humanwhocodes/config-array": "^0.11.13", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", + "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..14d98be --- /dev/null +++ b/js/package.json @@ -0,0 +1,29 @@ +{ + "name": "evmole", + "version": "0.0.1", + "description": "Extracts function selectors from EVM bytecode", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts", + "type": "module", + "scripts": { + "build-cjs": "npx esbuild ./src/ --bundle --minify --sourcemap --format=cjs --outfile=dist/index.cjs.js", + "build-esm": "npx esbuild ./src/ --bundle --minify --sourcemap --format=esm --outfile=dist/index.esm.js", + "build": "rm -rf dist/ && npm run build-cjs && npm run build-esm && cp ./src/index.d.ts dist/", + "lint": "npx eslint ./src/", + "format": "npx prettier ./src/ --write" + }, + "author": "Maxim Andreev ", + "license": "MIT", + "homepage": "https://github.com/cdump/evmole", + "repository": "github:cdump/evmole", + "files": [ + "dist", + "README.md" + ], + "devDependencies": { + "esbuild": "0.19.6", + "eslint": "^8.53.0", + "prettier": "3.1.0" + } +} diff --git a/js/src/evm/memory.js b/js/src/evm/memory.js new file mode 100644 index 0000000..e4c4af5 --- /dev/null +++ b/js/src/evm/memory.js @@ -0,0 +1,53 @@ +export default class Memory { + constructor() { + this._data = [] + } + + toString() { + let r = `${this._data.length} elems:\n` + for (const el of this._data) { + r += ` - ${el.toString(16)} | ${typeof el}\n` + } + return r + } + + store(offset, value) { + this._data.push([offset, value]) + } + + load(offset) { + this._data = this._data.sort((a, b) => a[0] - b[0]) + let ret = [] + let used = [] + + for (const [off, val] of this._data) { + const b = off + val.length + if (b <= offset) { + continue + } + if (offset + (32 - ret.length) <= off) { + break + } + + if (off > offset) { + ret.push(...new Array(off - offset).fill(0)) + ret.push(...val) + } else if (off < offset) { + ret.push(...val.subarray(offset - off)) + } else { + ret.push(...val) + } + + used.push(val) + offset += b + } + + if (ret.length > 32) { + ret = ret.slice(0, 32) + } else { + ret.push(...new Array(32 - ret.length).fill(0)) + } + + return [new Uint8Array(ret), used] + } +} diff --git a/js/src/evm/opcodes.js b/js/src/evm/opcodes.js new file mode 100644 index 0000000..c74ef25 --- /dev/null +++ b/js/src/evm/opcodes.js @@ -0,0 +1,333 @@ +export default class Op { + constructor( + code, + name = '', + stack_in = 0, + stack_out = 0, + blen = 1, + gas = undefined, + ) { + this.code = code + this.name = name + this.stack_in = stack_in + this.stack_out = stack_out + this.blen = blen + this.gas = gas + } + + valueOf() { + return this.code + } + + static parse(code) { + const op = this.#ops[code] + if (op === undefined) throw `Unknown op code ${code}` + return op + } + + static STOP = new Op(0x00, 'STOP', 0, 0, 1, 0) + static ADD = new Op(0x01, 'ADD', 2, 1, 1, 3) + static MUL = new Op(0x02, 'MUL', 2, 1, 1, 5) + static SUB = new Op(0x03, 'SUB', 2, 1, 1, 3) + static DIV = new Op(0x04, 'DIV', 2, 1, 1, 5) + static SDIV = new Op(0x05, 'SDIV', 2, 1, 1, 5) + static MOD = new Op(0x06, 'MOD', 2, 1, 1, 5) + static SMOD = new Op(0x07, 'SMOD', 2, 1, 1, 5) + static ADDMOD = new Op(0x08, 'ADDMOD', 3, 1, 1, 8) + static MULMOD = new Op(0x09, 'MULMOD', 3, 1, 1, 8) + static EXP = new Op(0x0a, 'EXP', 2, 1, 1, undefined) + static SIGNEXTEND = new Op(0x0b, 'SIGNEXTEND', 2, 1, 1, 5) + + static LT = new Op(0x10, 'LT', 2, 1, 1, 3) + static GT = new Op(0x11, 'GT', 2, 1, 1, 3) + static SLT = new Op(0x12, 'SLT', 2, 1, 1, 3) + static SGT = new Op(0x13, 'SGT', 2, 1, 1, 3) + static EQ = new Op(0x14, 'EQ', 2, 1, 1, 3) + static ISZERO = new Op(0x15, 'ISZERO', 1, 1, 1, 3) + static AND = new Op(0x16, 'AND', 2, 1, 1, 3) + static OR = new Op(0x17, 'OR', 2, 1, 1, 3) + static XOR = new Op(0x18, 'XOR', 2, 1, 1, 3) + static NOT = new Op(0x19, 'NOT', 1, 1, 1, 3) + static BYTE = new Op(0x1a, 'BYTE', 2, 1, 1, 3) + static SHL = new Op(0x1b, 'SHL', 2, 1, 1, 3) + static SHR = new Op(0x1c, 'SHR', 2, 1, 1, 3) + static SAR = new Op(0x1d, 'SAR', 2, 1, 1, 3) + + static KECCAK256 = new Op(0x20, 'KECCAK256', 2, 1, 1, undefined) + + static ADDRESS = new Op(0x30, 'ADDRESS', 0, 1, 1, 2) + static BALANCE = new Op(0x31, 'BALANCE', 1, 1, 1, undefined) + static ORIGIN = new Op(0x32, 'ORIGIN', 0, 1, 1, 2) + static CALLER = new Op(0x33, 'CALLER', 0, 1, 1, 2) + static CALLVALUE = new Op(0x34, 'CALLVALUE', 0, 1, 1, 2) + static CALLDATALOAD = new Op(0x35, 'CALLDATALOAD', 1, 1, 1, 3) + static CALLDATASIZE = new Op(0x36, 'CALLDATASIZE', 0, 1, 1, 2) + static CALLDATACOPY = new Op(0x37, 'CALLDATACOPY', 3, 0, 1, undefined) + static CODESIZE = new Op(0x38, 'CODESIZE', 0, 1, 1, 2) + static CODECOPY = new Op(0x39, 'CODECOPY', 3, 0, 1, undefined) + static GASPRICE = new Op(0x3a, 'GASPRICE', 0, 1, 1, 2) + static EXTCODESIZE = new Op(0x3b, 'EXTCODESIZE', 1, 1, 1, undefined) + static EXTCODECOPY = new Op(0x3c, 'EXTCODECOPY', 4, 0, 1, undefined) + static RETURNDATASIZE = new Op(0x3d, 'RETURNDATASIZE', 0, 1, 1, undefined) + static RETURNDATACOPY = new Op(0x3e, 'RETURNDATACOPY', 3, 0, 1, undefined) + static EXTCODEHASH = new Op(0x3f, 'EXTCODEHASH', 1, 1, 1, undefined) + + static BLOCKHASH = new Op(0x40, 'BLOCKHASH', 1, 1, 1, 20) + static COINBASE = new Op(0x41, 'COINBASE', 0, 1, 1, 2) + static TIMESTAMP = new Op(0x42, 'TIMESTAMP', 0, 1, 1, 2) + static NUMBER = new Op(0x43, 'NUMBER', 0, 1, 1, 2) + static DIFFICULTY = new Op(0x44, 'DIFFICULTY', 0, 1, 1, 2) + static GASLIMIT = new Op(0x45, 'GASLIMIT', 0, 1, 1, 2) + static CHAINID = new Op(0x46, 'CHAINID', 0, 1, 1, 2) + static SELFBALANCE = new Op(0x47, 'SELFBALANCE', 0, 1, 1, 5) + static BASEFEE = new Op(0x48, 'BASEFEE', 0, 1, 1, 2) + + static POP = new Op(0x50, 'POP', 1, 0, 1, 2) + static MLOAD = new Op(0x51, 'MLOAD', 1, 1, 1, undefined) + static MSTORE = new Op(0x52, 'MSTORE', 2, 0, 1, undefined) + static MSTORE8 = new Op(0x53, 'MSTORE8', 2, 0, 1, undefined) + static SLOAD = new Op(0x54, 'SLOAD', 1, 1, 1, undefined) + static SSTORE = new Op(0x55, 'SSTORE', 2, 0, 1, undefined) + static JUMP = new Op(0x56, 'JUMP', 1, 0, 1, 8) + static JUMPI = new Op(0x57, 'JUMPI', 2, 0, 1, 10) + static PC = new Op(0x58, 'PC', 0, 1, 1, 2) + static MSIZE = new Op(0x59, 'MSIZE', 0, 1, 1, 2) + static GAS = new Op(0x5a, 'GAS', 0, 1, 1, 2) + static JUMPDEST = new Op(0x5b, 'JUMPDEST', 0, 0, 1, 1) + static PUSH0 = new Op(0x5f, 'PUSH0', 0, 1, 1, 2) + + static PUSH1 = new Op(0x60, 'PUSH1', 0, 1, 2, 3) + static PUSH2 = new Op(0x61, 'PUSH2', 0, 1, 3, 3) + static PUSH3 = new Op(0x62, 'PUSH3', 0, 1, 4, 3) + static PUSH4 = new Op(0x63, 'PUSH4', 0, 1, 5, 3) + static PUSH5 = new Op(0x64, 'PUSH5', 0, 1, 6, 3) + static PUSH6 = new Op(0x65, 'PUSH6', 0, 1, 7, 3) + static PUSH7 = new Op(0x66, 'PUSH7', 0, 1, 8, 3) + static PUSH8 = new Op(0x67, 'PUSH8', 0, 1, 9, 3) + static PUSH9 = new Op(0x68, 'PUSH9', 0, 1, 10, 3) + static PUSH10 = new Op(0x69, 'PUSH10', 0, 1, 11, 3) + static PUSH11 = new Op(0x6a, 'PUSH11', 0, 1, 12, 3) + static PUSH12 = new Op(0x6b, 'PUSH12', 0, 1, 13, 3) + static PUSH13 = new Op(0x6c, 'PUSH13', 0, 1, 14, 3) + static PUSH14 = new Op(0x6d, 'PUSH14', 0, 1, 15, 3) + static PUSH15 = new Op(0x6e, 'PUSH15', 0, 1, 16, 3) + static PUSH16 = new Op(0x6f, 'PUSH16', 0, 1, 17, 3) + + static PUSH17 = new Op(0x70, 'PUSH17', 0, 1, 18, 3) + static PUSH18 = new Op(0x71, 'PUSH18', 0, 1, 19, 3) + static PUSH19 = new Op(0x72, 'PUSH19', 0, 1, 20, 3) + static PUSH20 = new Op(0x73, 'PUSH20', 0, 1, 21, 3) + static PUSH21 = new Op(0x74, 'PUSH21', 0, 1, 22, 3) + static PUSH22 = new Op(0x75, 'PUSH22', 0, 1, 23, 3) + static PUSH23 = new Op(0x76, 'PUSH23', 0, 1, 24, 3) + static PUSH24 = new Op(0x77, 'PUSH24', 0, 1, 25, 3) + static PUSH25 = new Op(0x78, 'PUSH25', 0, 1, 26, 3) + static PUSH26 = new Op(0x79, 'PUSH26', 0, 1, 27, 3) + static PUSH27 = new Op(0x7a, 'PUSH27', 0, 1, 28, 3) + static PUSH28 = new Op(0x7b, 'PUSH28', 0, 1, 29, 3) + static PUSH29 = new Op(0x7c, 'PUSH29', 0, 1, 30, 3) + static PUSH30 = new Op(0x7d, 'PUSH30', 0, 1, 31, 3) + static PUSH31 = new Op(0x7e, 'PUSH31', 0, 1, 32, 3) + static PUSH32 = new Op(0x7f, 'PUSH32', 0, 1, 33, 3) + + static DUP1 = new Op(0x80, 'DUP1', 1, 2, 1, 3) + static DUP2 = new Op(0x81, 'DUP2', 2, 3, 1, 3) + static DUP3 = new Op(0x82, 'DUP3', 3, 4, 1, 3) + static DUP4 = new Op(0x83, 'DUP4', 4, 5, 1, 3) + static DUP5 = new Op(0x84, 'DUP5', 5, 6, 1, 3) + static DUP6 = new Op(0x85, 'DUP6', 6, 7, 1, 3) + static DUP7 = new Op(0x86, 'DUP7', 7, 8, 1, 3) + static DUP8 = new Op(0x87, 'DUP8', 8, 9, 1, 3) + static DUP9 = new Op(0x88, 'DUP9', 9, 10, 1, 3) + static DUP10 = new Op(0x89, 'DUP10', 10, 11, 1, 3) + static DUP11 = new Op(0x8a, 'DUP11', 11, 12, 1, 3) + static DUP12 = new Op(0x8b, 'DUP12', 12, 13, 1, 3) + static DUP13 = new Op(0x8c, 'DUP13', 13, 14, 1, 3) + static DUP14 = new Op(0x8d, 'DUP14', 14, 15, 1, 3) + static DUP15 = new Op(0x8e, 'DUP15', 15, 16, 1, 3) + static DUP16 = new Op(0x8f, 'DUP16', 16, 17, 1, 3) + + static SWAP1 = new Op(0x90, 'SWAP1', 2, 2, 1, 3) + static SWAP2 = new Op(0x91, 'SWAP2', 3, 3, 1, 3) + static SWAP3 = new Op(0x92, 'SWAP3', 4, 4, 1, 3) + static SWAP4 = new Op(0x93, 'SWAP4', 5, 5, 1, 3) + static SWAP5 = new Op(0x94, 'SWAP5', 6, 6, 1, 3) + static SWAP6 = new Op(0x95, 'SWAP6', 7, 7, 1, 3) + static SWAP7 = new Op(0x96, 'SWAP7', 8, 8, 1, 3) + static SWAP8 = new Op(0x97, 'SWAP8', 9, 9, 1, 3) + static SWAP9 = new Op(0x98, 'SWAP9', 10, 10, 1, 3) + static SWAP10 = new Op(0x99, 'SWAP10', 11, 11, 1, 3) + static SWAP11 = new Op(0x9a, 'SWAP11', 12, 12, 1, 3) + static SWAP12 = new Op(0x9b, 'SWAP12', 13, 13, 1, 3) + static SWAP13 = new Op(0x9c, 'SWAP13', 14, 14, 1, 3) + static SWAP14 = new Op(0x9d, 'SWAP14', 15, 15, 1, 3) + static SWAP15 = new Op(0x9e, 'SWAP15', 16, 16, 1, 3) + static SWAP16 = new Op(0x9f, 'SWAP16', 17, 17, 1, 3) + + static LOG0 = new Op(0xa0, 'LOG0', 2, 0, 1, undefined) + static LOG1 = new Op(0xa1, 'LOG1', 3, 0, 1, undefined) + static LOG2 = new Op(0xa2, 'LOG2', 4, 0, 1, undefined) + static LOG3 = new Op(0xa3, 'LOG3', 5, 0, 1, undefined) + static LOG4 = new Op(0xa4, 'LOG4', 6, 0, 1, undefined) + + static CREATE = new Op(0xf0, 'CREATE', 3, 1, 1, undefined) + static CALL = new Op(0xf1, 'CALL', 7, 1, 1, undefined) + static CALLCODE = new Op(0xf2, 'CALLCODE', 7, 1, 1, undefined) + static RETURN = new Op(0xf3, 'RETURN', 2, 0, 1, undefined) + static DELEGATECALL = new Op(0xf4, 'DELEGATECALL', 6, 1, 1, undefined) + static CREATE2 = new Op(0xf5, 'CREATE2', 4, 1, 1, undefined) + + static STATICCALL = new Op(0xfa, 'STATICCALL', 6, 1, 1, undefined) + static REVERT = new Op(0xfd, 'REVERT', 2, 0, 1, undefined) + static INVALID = new Op(0xfe, 'INVALID', 0, 0, 1, undefined) + static SELFDESTRUCT = new Op(0xff, 'SELFDESTRUCT', 1, 0, 1, undefined) + + static #ops = Array(256) + static { + const o = this.#ops + o[0x00] = Op.STOP + o[0x01] = Op.ADD + o[0x02] = Op.MUL + o[0x03] = Op.SUB + o[0x04] = Op.DIV + o[0x05] = Op.SDIV + o[0x06] = Op.MOD + o[0x07] = Op.SMOD + o[0x08] = Op.ADDMOD + o[0x09] = Op.MULMOD + o[0x0a] = Op.EXP + o[0x0b] = Op.SIGNEXTEND + o[0x10] = Op.LT + o[0x11] = Op.GT + o[0x12] = Op.SLT + o[0x13] = Op.SGT + o[0x14] = Op.EQ + o[0x15] = Op.ISZERO + o[0x16] = Op.AND + o[0x17] = Op.OR + o[0x18] = Op.XOR + o[0x19] = Op.NOT + o[0x1a] = Op.BYTE + o[0x1b] = Op.SHL + o[0x1c] = Op.SHR + o[0x1d] = Op.SAR + o[0x20] = Op.KECCAK256 + o[0x30] = Op.ADDRESS + o[0x31] = Op.BALANCE + o[0x32] = Op.ORIGIN + o[0x33] = Op.CALLER + o[0x34] = Op.CALLVALUE + o[0x35] = Op.CALLDATALOAD + o[0x36] = Op.CALLDATASIZE + o[0x37] = Op.CALLDATACOPY + o[0x38] = Op.CODESIZE + o[0x39] = Op.CODECOPY + o[0x3a] = Op.GASPRICE + o[0x3b] = Op.EXTCODESIZE + o[0x3c] = Op.EXTCODECOPY + o[0x3d] = Op.RETURNDATASIZE + o[0x3e] = Op.RETURNDATACOPY + o[0x3f] = Op.EXTCODEHASH + o[0x40] = Op.BLOCKHASH + o[0x41] = Op.COINBASE + o[0x42] = Op.TIMESTAMP + o[0x43] = Op.NUMBER + o[0x44] = Op.DIFFICULTY + o[0x45] = Op.GASLIMIT + o[0x46] = Op.CHAINID + o[0x47] = Op.SELFBALANCE + o[0x48] = Op.BASEFEE + o[0x50] = Op.POP + o[0x51] = Op.MLOAD + o[0x52] = Op.MSTORE + o[0x53] = Op.MSTORE8 + o[0x54] = Op.SLOAD + o[0x55] = Op.SSTORE + o[0x56] = Op.JUMP + o[0x57] = Op.JUMPI + o[0x58] = Op.PC + o[0x59] = Op.MSIZE + o[0x5a] = Op.GAS + o[0x5b] = Op.JUMPDEST + o[0x5f] = Op.PUSH0 + o[0x60] = Op.PUSH1 + o[0x61] = Op.PUSH2 + o[0x62] = Op.PUSH3 + o[0x63] = Op.PUSH4 + o[0x64] = Op.PUSH5 + o[0x65] = Op.PUSH6 + o[0x66] = Op.PUSH7 + o[0x67] = Op.PUSH8 + o[0x68] = Op.PUSH9 + o[0x69] = Op.PUSH10 + o[0x6a] = Op.PUSH11 + o[0x6b] = Op.PUSH12 + o[0x6c] = Op.PUSH13 + o[0x6d] = Op.PUSH14 + o[0x6e] = Op.PUSH15 + o[0x6f] = Op.PUSH16 + o[0x70] = Op.PUSH17 + o[0x71] = Op.PUSH18 + o[0x72] = Op.PUSH19 + o[0x73] = Op.PUSH20 + o[0x74] = Op.PUSH21 + o[0x75] = Op.PUSH22 + o[0x76] = Op.PUSH23 + o[0x77] = Op.PUSH24 + o[0x78] = Op.PUSH25 + o[0x79] = Op.PUSH26 + o[0x7a] = Op.PUSH27 + o[0x7b] = Op.PUSH28 + o[0x7c] = Op.PUSH29 + o[0x7d] = Op.PUSH30 + o[0x7e] = Op.PUSH31 + o[0x7f] = Op.PUSH32 + o[0x80] = Op.DUP1 + o[0x81] = Op.DUP2 + o[0x82] = Op.DUP3 + o[0x83] = Op.DUP4 + o[0x84] = Op.DUP5 + o[0x85] = Op.DUP6 + o[0x86] = Op.DUP7 + o[0x87] = Op.DUP8 + o[0x88] = Op.DUP9 + o[0x89] = Op.DUP10 + o[0x8a] = Op.DUP11 + o[0x8b] = Op.DUP12 + o[0x8c] = Op.DUP13 + o[0x8d] = Op.DUP14 + o[0x8e] = Op.DUP15 + o[0x8f] = Op.DUP16 + o[0x90] = Op.SWAP1 + o[0x91] = Op.SWAP2 + o[0x92] = Op.SWAP3 + o[0x93] = Op.SWAP4 + o[0x94] = Op.SWAP5 + o[0x95] = Op.SWAP6 + o[0x96] = Op.SWAP7 + o[0x97] = Op.SWAP8 + o[0x98] = Op.SWAP9 + o[0x99] = Op.SWAP10 + o[0x9a] = Op.SWAP11 + o[0x9b] = Op.SWAP12 + o[0x9c] = Op.SWAP13 + o[0x9d] = Op.SWAP14 + o[0x9e] = Op.SWAP15 + o[0x9f] = Op.SWAP16 + o[0xa0] = Op.LOG0 + o[0xa1] = Op.LOG1 + o[0xa2] = Op.LOG2 + o[0xa3] = Op.LOG3 + o[0xa4] = Op.LOG4 + o[0xf0] = Op.CREATE + o[0xf1] = Op.CALL + o[0xf2] = Op.CALLCODE + o[0xf3] = Op.RETURN + o[0xf4] = Op.DELEGATECALL + o[0xf5] = Op.CREATE2 + o[0xfa] = Op.STATICCALL + o[0xfd] = Op.REVERT + o[0xfe] = Op.INVALID + o[0xff] = Op.SELFDESTRUCT + } +} diff --git a/js/src/evm/stack.js b/js/src/evm/stack.js new file mode 100644 index 0000000..d8afdac --- /dev/null +++ b/js/src/evm/stack.js @@ -0,0 +1,38 @@ +export default class Stack { + constructor() { + this._data = [] + } + + toString() { + let r = `${this._data.length} elems:\n` + for (const el of this._data) { + r += ` - ${el.toString(16)} | ${typeof el}\n` + } + return r + } + + push(val) { + this._data.push(val) + } + + pop() { + return this._data.pop() + } + + peek(idx = 0) { + if (this._data.length < idx + 1) { + return undefined + } + return this._data[this._data.length - idx - 1] + } + + dup(n) { + this._data.push(this._data[this._data.length - n]) + } + + swap(n) { + const tmp = this._data[this._data.length - n - 1] + this._data[this._data.length - n - 1] = this._data[this._data.length - 1] + this._data[this._data.length - 1] = tmp + } +} diff --git a/js/src/evm/vm.js b/js/src/evm/vm.js new file mode 100644 index 0000000..f01c3fd --- /dev/null +++ b/js/src/evm/vm.js @@ -0,0 +1,223 @@ +import Op from './opcodes.js' +import Stack from './stack.js' +import Memory from './memory.js' +import { uint8ArrayToBigInt, hexToUint8Array, modExp } from '../utils.js' + +const E256 = 2n ** 256n +const E256M1 = E256 - 1n + +function toBigInt(v) { + if (typeof v === 'bigint') return v + if (typeof v.toBigInt === 'function') return v.toBigInt() + if (!(v instanceof Uint8Array)) throw `Not uint8array instance` + return uint8ArrayToBigInt(v) +} + +export default class Vm { + constructor(code, calldata, clone = false) { + if (clone) { + return + } + this.code = code + this.pc = 0 + this.stack = new Stack() + this.memory = new Memory() + this.stopped = false + this.calldata = calldata + } + + toString() { + let r = 'Vm:\n' + r += ` .pc = 0x${this.pc.toString(16)} | ${this.current_op().name}\n` + r += ` .stack = ${this.stack}\n` + return r + } + + clone() { + const c = new Vm(0, 0, true) + c.code = this.code + c.pc = this.pc + c.stack = new Stack() + c.stack._data = [...this.stack._data] + c.memory = new Memory() + c.memory._data = [...this.memory._data] + c.stopped = this.stopped + c.calldata = this.calldata + return c + } + + current_op() { + return Op.parse(this.code[this.pc]) + } + + step() { + const ret = this.#exec_next_opcode() + const op = ret[0] + if (ret[1] == -1) { + throw `Op ${op.name} with unset gas_used` + } + + if (op != Op.JUMP && op != Op.JUMPI) { + this.pc += op.blen + } + if (this.pc >= this.code.length) { + this.stopped = true + } + return ret + } + + #exec_next_opcode() { + const op = this.current_op() + let gas_used = op.gas !== undefined ? op.gas : -1 + + if (op >= Op.PUSH0 && op <= Op.PUSH32) { + const n = op - Op.PUSH0 + if (n != 0) { + const args = this.code.subarray(this.pc + 1, this.pc + 1 + n) + this.stack.push(uint8ArrayToBigInt(args)) + } else { + this.stack.push(0n) + } + return [op, gas_used] + } + if (op >= Op.DUP1 && op <= Op.DUP16) { + this.stack.dup(op - Op.DUP1 + 1) + return [op, gas_used] + } + if (op >= Op.SWAP1 && op <= Op.SWAP16) { + this.stack.swap(op - Op.SWAP1 + 1) + return [op, gas_used] + } + + switch (op) { + case Op.JUMP: + case Op.JUMPI: { + const s0 = Number(this.stack.pop()) + if (this.code[s0] != Op.JUMPDEST.code) { + throw 'jump to not JUMPDEST' + } + if (op == Op.JUMPI) { + const s1 = this.stack.pop() + if (s1 == 0n) { + this.pc += 1 + return [op, gas_used] + } + } + this.pc = Number(s0) + return [op, gas_used] + } + + case Op.JUMPDEST: + return [op, gas_used] + + case Op.REVERT: + this.stack.pop() + this.stack.pop() + this.stopped = true + return [op, 4] + + case Op.ISZERO: { + const raw = this.stack.pop() + const v = toBigInt(raw) + this.stack.push(v === 0n ? 1n : 0n) + return [op, gas_used, raw] + } + + case Op.POP: + this.stack.pop() + return [op] + + case Op.LT: + case Op.GT: + case Op.EQ: + case Op.SUB: + case Op.DIV: + case Op.EXP: + case Op.XOR: + case Op.AND: + case Op.SHR: { + const raws0 = this.stack.pop() + const raws1 = this.stack.pop() + + const s0 = toBigInt(raws0) + const s1 = toBigInt(raws1) + + let res + switch (op) { + case Op.EQ: + res = s0 == s1 ? 1n : 0n + break + case Op.GT: + res = s0 > s1 ? 1n : 0n + break + case Op.LT: + res = s0 < s1 ? 1n : 0n + break + case Op.SUB: + res = (s0 - s1) & E256M1 + break + case Op.DIV: + res = s1 != 0n ? s0 / s1 : 0n + break + case Op.EXP: + res = modExp(s0, s1, E256) + gas_used = 50 * (1 + Math.floor(s1.toString(2).length / 8)) // ~approx + break + case Op.XOR: + res = s0 ^ s1 + break + case Op.AND: + res = s0 & s1 + break + case Op.SHR: + res = (s1 >> s0) & E256M1 + break + } + this.stack.push(res) + return [op, gas_used, raws0, raws1] + } + + case Op.CALLVALUE: + this.stack.push(0n) // msg.value == 0 + return [op, gas_used] + + case Op.CALLDATALOAD: { + const offset = Number(this.stack.pop()) + this.stack.push(this.calldata.load(offset)) + return [op, gas_used] + } + + case Op.CALLDATASIZE: + this.stack.push(BigInt(this.calldata.length)) + return [op, gas_used] + + case Op.MSTORE: { + const offset = Number(this.stack.pop()) + const raw = this.stack.pop() + const v = + typeof raw === 'bigint' ? hexToUint8Array(raw.toString(16)) : raw + this.memory.store(offset, v) + return [op, 3] + } + + case Op.MLOAD: { + const offset = Number(this.stack.pop()) + const [val, used] = this.memory.load(offset) + this.stack.push(uint8ArrayToBigInt(val)) + return [op, 4, used] + } + + case Op.CALLDATACOPY: { + const mem_off = Number(this.stack.pop()) + const src_off = Number(this.stack.pop()) + const size = Number(this.stack.pop()) + const value = this.calldata.load(src_off, size) + this.memory.store(mem_off, value) + return [op, 4] + } + + default: + throw `unknown op ${op.name}` + } + } +} diff --git a/js/src/index.d.ts b/js/src/index.d.ts new file mode 100644 index 0000000..2283452 --- /dev/null +++ b/js/src/index.d.ts @@ -0,0 +1,4 @@ +export function functionSelectors( + code_hex_string: string, + gas_limit: number = 1e6, +) diff --git a/js/src/index.js b/js/src/index.js new file mode 100644 index 0000000..1fbf453 --- /dev/null +++ b/js/src/index.js @@ -0,0 +1,121 @@ +import Op from './evm/opcodes.js' +import Vm from './evm/vm.js' + +import { + hexToUint8Array, + bigIntToUint8Array, + uint8ArrayToBigInt, +} from './utils.js' + +class CallData extends Uint8Array { + load(offset, size = 32) { + const v = new CallData(32) + v.set(this.subarray(offset, offset + size)) + return v + } + toBigInt() { + return uint8ArrayToBigInt(this) + } +} + +class CallDataSignature extends Uint8Array { + toBigInt() { + return uint8ArrayToBigInt(this) + } +} + +function process(vm, gas_limit) { + let selectors = [] + let gas_used = 0 + + while (!vm.stopped) { + // console.log(vm.toString()); + let ret + try { + ret = vm.step() + gas_used += ret[1] + if (gas_used > gas_limit) { + throw `gas overflow: ${gas_used} > ${gas_limit}` + } + } catch (err) { + // console.log(err); + // throw err; + break + } + const op = ret[0] + + if (op == Op.EQ || op == Op.XOR) { + if (ret[2] instanceof CallDataSignature) { + selectors.push(ret[3]) + vm.stack.pop() + vm.stack.push(op == Op.XOR ? 1n : 0n) + } else if (ret[3] instanceof CallDataSignature) { + selectors.push(ret[2]) + vm.stack.pop() + vm.stack.push(op == Op.XOR ? 1n : 0n) + } + continue + } + + if (op == Op.SUB) { + if (ret[2] instanceof CallDataSignature) { + selectors.push(ret[3]) + } else if (ret[3] instanceof CallDataSignature) { + selectors.push(ret[2]) + } + continue + } + + if (op == Op.LT || op == Op.GT) { + const cloned_vm = vm.clone() + const [s, gas] = process(cloned_vm, gas_limit / 2) + selectors.push(...s) + gas_used += gas + const v = vm.stack.pop() + vm.stack.push(v === 0n ? 1n : 0n) + continue + } + + if (op == Op.SHR || op == Op.AND || op == Op.DIV) { + const x = vm.stack.peek() + if (x === undefined) throw 'stack peek failed' + if ((x & 0xffffffffn) == vm.calldata.toBigInt()) { + const v = vm.stack.pop() + vm.stack.push(new CallDataSignature(bigIntToUint8Array(v))) + } + continue + } + + if (op == Op.ISZERO) { + if (ret[2] instanceof CallDataSignature) { + selectors.push(0n) + } + continue + } + + if (op == Op.MLOAD) { + const used = ret[2] + for (const u of used) { + if (u instanceof CallData) { + const v = vm.stack.peek() + if ((v & 0xffffffffn) == vm.calldata.toBigInt()) { + vm.stack.push( + new CallDataSignature(bigIntToUint8Array(vm.stack.pop())), + ) + break + } + } + } + continue + } + } + + return [selectors, gas_used] +} + +export function functionSelectors(code_hex_string, gas_limit = 1e6) { + const code = hexToUint8Array(code_hex_string) + const vm = new Vm(code, new CallData([0xaa, 0xbb, 0xcc, 0xdd])) + const [selectors] = process(vm, gas_limit) + return selectors.map((x) => x.toString(16).padStart(8, '0')) +} diff --git a/js/src/utils.js b/js/src/utils.js new file mode 100644 index 0000000..45bb8e0 --- /dev/null +++ b/js/src/utils.js @@ -0,0 +1,40 @@ +export function hexToUint8Array(str) { + let start = 0 + if (str.startsWith('0x')) { + start = 2 + } + const arr = new Uint8Array((str.length - start) / 2) + let p = 0 + for (let i = start; i < str.length; i += 2) { + arr[p] = parseInt(str.slice(i, i + 2), 16) + p++ + } + return arr +} + +export function uint8ArrayToBigInt(arr) { + return BigInt( + arr.reduce((acc, v) => acc + v.toString(16).padStart(2, '0'), '0x'), + ) +} + +export function bigIntToUint8Array(val) { + return hexToUint8Array(val.toString(16)) +} + +export function modExp(a, b, n) { + a = a % n + var result = 1n + var x = a + while (b > 0) { + var leastSignificantBit = b % 2n + b = b / 2n + if (leastSignificantBit == 1n) { + result = result * x + result = result % n + } + x = x * x + x = x % n + } + return result +} diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b13f3e5 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,707 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "aiohttp" +version = "3.9.0" +description = "Async http client/server framework (asyncio)" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.9.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6896b8416be9ada4d22cd359d7cb98955576ce863eadad5596b7cdfbf3e17c6c"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1736d87dad8ef46a8ec9cddd349fa9f7bd3a064c47dd6469c0d6763d3d49a4fc"}, + {file = "aiohttp-3.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c9e5f4d7208cda1a2bb600e29069eecf857e6980d0ccc922ccf9d1372c16f4b"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8488519aa05e636c5997719fe543c8daf19f538f4fa044f3ce94bee608817cff"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ab16c254e2312efeb799bc3c06897f65a133b38b69682bf75d1f1ee1a9c43a9"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7a94bde005a8f926d0fa38b88092a03dea4b4875a61fbcd9ac6f4351df1b57cd"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b777c9286b6c6a94f50ddb3a6e730deec327e9e2256cb08b5530db0f7d40fd8"}, + {file = "aiohttp-3.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:571760ad7736b34d05597a1fd38cbc7d47f7b65deb722cb8e86fd827404d1f6b"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:deac0a32aec29608eb25d730f4bc5a261a65b6c48ded1ed861d2a1852577c932"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4ee1b4152bc3190cc40ddd6a14715e3004944263ea208229ab4c297712aa3075"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:3607375053df58ed6f23903aa10cf3112b1240e8c799d243bbad0f7be0666986"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:65b0a70a25456d329a5e1426702dde67be0fb7a4ead718005ba2ca582d023a94"}, + {file = "aiohttp-3.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5a2eb5311a37fe105aa35f62f75a078537e1a9e4e1d78c86ec9893a3c97d7a30"}, + {file = "aiohttp-3.9.0-cp310-cp310-win32.whl", hash = "sha256:2cbc14a13fb6b42d344e4f27746a4b03a2cb0c1c3c5b932b0d6ad8881aa390e3"}, + {file = "aiohttp-3.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ac9669990e2016d644ba8ae4758688534aabde8dbbc81f9af129c3f5f01ca9cd"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f8e05f5163528962ce1d1806fce763ab893b1c5b7ace0a3538cd81a90622f844"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4afa8f71dba3a5a2e1e1282a51cba7341ae76585345c43d8f0e624882b622218"}, + {file = "aiohttp-3.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f929f4c9b9a00f3e6cc0587abb95ab9c05681f8b14e0fe1daecfa83ea90f8318"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28185e36a78d247c55e9fbea2332d16aefa14c5276a582ce7a896231c6b1c208"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a486ddf57ab98b6d19ad36458b9f09e6022de0381674fe00228ca7b741aacb2f"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70e851f596c00f40a2f00a46126c95c2e04e146015af05a9da3e4867cfc55911"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5b7bf8fe4d39886adc34311a233a2e01bc10eb4e842220235ed1de57541a896"}, + {file = "aiohttp-3.9.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c67a51ea415192c2e53e4e048c78bab82d21955b4281d297f517707dc836bf3d"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:694df243f394629bcae2d8ed94c589a181e8ba8604159e6e45e7b22e58291113"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3dd8119752dd30dd7bca7d4bc2a92a59be6a003e4e5c2cf7e248b89751b8f4b7"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:eb6dfd52063186ac97b4caa25764cdbcdb4b10d97f5c5f66b0fa95052e744eb7"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:d97c3e286d0ac9af6223bc132dc4bad6540b37c8d6c0a15fe1e70fb34f9ec411"}, + {file = "aiohttp-3.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:816f4db40555026e4cdda604a1088577c1fb957d02f3f1292e0221353403f192"}, + {file = "aiohttp-3.9.0-cp311-cp311-win32.whl", hash = "sha256:3abf0551874fecf95f93b58f25ef4fc9a250669a2257753f38f8f592db85ddea"}, + {file = "aiohttp-3.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:e18d92c3e9e22553a73e33784fcb0ed484c9874e9a3e96c16a8d6a1e74a0217b"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:99ae01fb13a618b9942376df77a1f50c20a281390dad3c56a6ec2942e266220d"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:05857848da443c8c12110d99285d499b4e84d59918a21132e45c3f0804876994"}, + {file = "aiohttp-3.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:317719d7f824eba55857fe0729363af58e27c066c731bc62cd97bc9c3d9c7ea4"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1e3b3c107ccb0e537f309f719994a55621acd2c8fdf6d5ce5152aed788fb940"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45820ddbb276113ead8d4907a7802adb77548087ff5465d5c554f9aa3928ae7d"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:05a183f1978802588711aed0dea31e697d760ce9055292db9dc1604daa9a8ded"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a4cd44788ea0b5e6bb8fa704597af3a30be75503a7ed1098bc5b8ffdf6c982"}, + {file = "aiohttp-3.9.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:673343fbc0c1ac44d0d2640addc56e97a052504beacd7ade0dc5e76d3a4c16e8"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7e8a3b79b6d186a9c99761fd4a5e8dd575a48d96021f220ac5b5fa856e5dd029"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6777a390e41e78e7c45dab43a4a0196c55c3b8c30eebe017b152939372a83253"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7ae5f99a32c53731c93ac3075abd3e1e5cfbe72fc3eaac4c27c9dd64ba3b19fe"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:f1e4f254e9c35d8965d377e065c4a8a55d396fe87c8e7e8429bcfdeeb229bfb3"}, + {file = "aiohttp-3.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11ca808f9a6b63485059f5f6e164ef7ec826483c1212a44f268b3653c91237d8"}, + {file = "aiohttp-3.9.0-cp312-cp312-win32.whl", hash = "sha256:de3cc86f4ea8b4c34a6e43a7306c40c1275e52bfa9748d869c6b7d54aa6dad80"}, + {file = "aiohttp-3.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca4fddf84ac7d8a7d0866664936f93318ff01ee33e32381a115b19fb5a4d1202"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f09960b5bb1017d16c0f9e9f7fc42160a5a49fa1e87a175fd4a2b1a1833ea0af"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8303531e2c17b1a494ffaeba48f2da655fe932c4e9a2626c8718403c83e5dd2b"}, + {file = "aiohttp-3.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4790e44f46a4aa07b64504089def5744d3b6780468c4ec3a1a36eb7f2cae9814"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1d7edf74a36de0e5ca50787e83a77cf352f5504eb0ffa3f07000a911ba353fb"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94697c7293199c2a2551e3e3e18438b4cba293e79c6bc2319f5fd652fccb7456"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a1b66dbb8a7d5f50e9e2ea3804b01e766308331d0cac76eb30c563ac89c95985"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9623cfd9e85b76b83ef88519d98326d4731f8d71869867e47a0b979ffec61c73"}, + {file = "aiohttp-3.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f32c86dc967ab8c719fd229ce71917caad13cc1e8356ee997bf02c5b368799bf"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f50b4663c3e0262c3a361faf440761fbef60ccdde5fe8545689a4b3a3c149fb4"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:dcf71c55ec853826cd70eadb2b6ac62ec577416442ca1e0a97ad875a1b3a0305"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:42fe4fd9f0dfcc7be4248c162d8056f1d51a04c60e53366b0098d1267c4c9da8"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76a86a9989ebf82ee61e06e2bab408aec4ea367dc6da35145c3352b60a112d11"}, + {file = "aiohttp-3.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f9e09a1c83521d770d170b3801eea19b89f41ccaa61d53026ed111cb6f088887"}, + {file = "aiohttp-3.9.0-cp38-cp38-win32.whl", hash = "sha256:a00ce44c21612d185c5275c5cba4bab8d7c1590f248638b667ed8a782fa8cd6f"}, + {file = "aiohttp-3.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:d5b9345ab92ebe6003ae11d8092ce822a0242146e6fa270889b9ba965457ca40"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:98d21092bf2637c5fa724a428a69e8f5955f2182bff61f8036827cf6ce1157bf"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:35a68cd63ca6aaef5707888f17a70c36efe62b099a4e853d33dc2e9872125be8"}, + {file = "aiohttp-3.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7f6235c7475658acfc1769d968e07ab585c79f6ca438ddfecaa9a08006aee2"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db04d1de548f7a62d1dd7e7cdf7c22893ee168e22701895067a28a8ed51b3735"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:536b01513d67d10baf6f71c72decdf492fb7433c5f2f133e9a9087379d4b6f31"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c8b0a6487e8109427ccf638580865b54e2e3db4a6e0e11c02639231b41fc0f"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7276fe0017664414fdc3618fca411630405f1aaf0cc3be69def650eb50441787"}, + {file = "aiohttp-3.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23170247ef89ffa842a02bbfdc425028574d9e010611659abeb24d890bc53bb8"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b1a2ea8252cacc7fd51df5a56d7a2bb1986ed39be9397b51a08015727dfb69bd"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d71abc15ff7047412ef26bf812dfc8d0d1020d664617f4913df2df469f26b76"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2d820162c8c2bdbe97d328cd4f417c955ca370027dce593345e437b2e9ffdc4d"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:2779f5e7c70f7b421915fd47db332c81de365678180a9f3ab404088f87ba5ff9"}, + {file = "aiohttp-3.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:366bc870d7ac61726f32a489fbe3d1d8876e87506870be66b01aeb84389e967e"}, + {file = "aiohttp-3.9.0-cp39-cp39-win32.whl", hash = "sha256:1df43596b826022b14998f0460926ce261544fedefe0d2f653e1b20f49e96454"}, + {file = "aiohttp-3.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:9c196b30f1b1aa3363a69dd69079ae9bec96c2965c4707eaa6914ba099fb7d4f"}, + {file = "aiohttp-3.9.0.tar.gz", hash = "sha256:09f23292d29135025e19e8ff4f0a68df078fe4ee013bca0105b2e803989de92d"}, +] + +[package.dependencies] +aiosignal = ">=1.1.2" +async-timeout = {version = ">=4.0,<5.0", markers = "python_version < \"3.11\""} +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "attrs" +version = "23.1.0" +description = "Classes Without Boilerplate" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, + {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, +] + +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] + +[[package]] +name = "black" +version = "23.11.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbea0bb8575c6b6303cc65017b46351dc5953eea5c0a59d7b7e3a2d2f433a911"}, + {file = "black-23.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:412f56bab20ac85927f3a959230331de5614aecda1ede14b373083f62ec24e6f"}, + {file = "black-23.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d136ef5b418c81660ad847efe0e55c58c8208b77a57a28a503a5f345ccf01394"}, + {file = "black-23.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:6c1cac07e64433f646a9a838cdc00c9768b3c362805afc3fce341af0e6a9ae9f"}, + {file = "black-23.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cf57719e581cfd48c4efe28543fea3d139c6b6f1238b3f0102a9c73992cbb479"}, + {file = "black-23.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:698c1e0d5c43354ec5d6f4d914d0d553a9ada56c85415700b81dc90125aac244"}, + {file = "black-23.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:760415ccc20f9e8747084169110ef75d545f3b0932ee21368f63ac0fee86b221"}, + {file = "black-23.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:58e5f4d08a205b11800332920e285bd25e1a75c54953e05502052738fe16b3b5"}, + {file = "black-23.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:45aa1d4675964946e53ab81aeec7a37613c1cb71647b5394779e6efb79d6d187"}, + {file = "black-23.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c44b7211a3a0570cc097e81135faa5f261264f4dfaa22bd5ee2875a4e773bd6"}, + {file = "black-23.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a9acad1451632021ee0d146c8765782a0c3846e0e0ea46659d7c4f89d9b212b"}, + {file = "black-23.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:fc7f6a44d52747e65a02558e1d807c82df1d66ffa80a601862040a43ec2e3142"}, + {file = "black-23.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7f622b6822f02bfaf2a5cd31fdb7cd86fcf33dab6ced5185c35f5db98260b055"}, + {file = "black-23.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:250d7e60f323fcfc8ea6c800d5eba12f7967400eb6c2d21ae85ad31c204fb1f4"}, + {file = "black-23.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5133f5507007ba08d8b7b263c7aa0f931af5ba88a29beacc4b2dc23fcefe9c06"}, + {file = "black-23.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:421f3e44aa67138ab1b9bfbc22ee3780b22fa5b291e4db8ab7eee95200726b07"}, + {file = "black-23.11.0-py3-none-any.whl", hash = "sha256:54caaa703227c6e0c87b76326d0862184729a69b73d3b7305b6288e1d830067e"}, + {file = "black-23.11.0.tar.gz", hash = "sha256:4c68855825ff432d197229846f971bc4d6666ce90492e5b02013bcaca4d9ab05"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "frozenlist" +version = "1.4.0" +description = "A list-like structure which implements collections.abc.MutableSequence" +category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:764226ceef3125e53ea2cb275000e309c0aa5464d43bd72abd661e27fffc26ab"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d6484756b12f40003c6128bfcc3fa9f0d49a687e171186c2d85ec82e3758c559"}, + {file = "frozenlist-1.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ac08e601308e41eb533f232dbf6b7e4cea762f9f84f6357136eed926c15d12c"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d081f13b095d74b67d550de04df1c756831f3b83dc9881c38985834387487f1b"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71932b597f9895f011f47f17d6428252fc728ba2ae6024e13c3398a087c2cdea"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:981b9ab5a0a3178ff413bca62526bb784249421c24ad7381e39d67981be2c326"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e41f3de4df3e80de75845d3e743b3f1c4c8613c3997a912dbf0229fc61a8b963"}, + {file = "frozenlist-1.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6918d49b1f90821e93069682c06ffde41829c346c66b721e65a5c62b4bab0300"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0e5c8764c7829343d919cc2dfc587a8db01c4f70a4ebbc49abde5d4b158b007b"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8d0edd6b1c7fb94922bf569c9b092ee187a83f03fb1a63076e7774b60f9481a8"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e29cda763f752553fa14c68fb2195150bfab22b352572cb36c43c47bedba70eb"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:0c7c1b47859ee2cac3846fde1c1dc0f15da6cec5a0e5c72d101e0f83dcb67ff9"}, + {file = "frozenlist-1.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:901289d524fdd571be1c7be054f48b1f88ce8dddcbdf1ec698b27d4b8b9e5d62"}, + {file = "frozenlist-1.4.0-cp310-cp310-win32.whl", hash = "sha256:1a0848b52815006ea6596c395f87449f693dc419061cc21e970f139d466dc0a0"}, + {file = "frozenlist-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:b206646d176a007466358aa21d85cd8600a415c67c9bd15403336c331a10d956"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:de343e75f40e972bae1ef6090267f8260c1446a1695e77096db6cfa25e759a95"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad2a9eb6d9839ae241701d0918f54c51365a51407fd80f6b8289e2dfca977cc3"}, + {file = "frozenlist-1.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bd7bd3b3830247580de99c99ea2a01416dfc3c34471ca1298bccabf86d0ff4dc"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdf1847068c362f16b353163391210269e4f0569a3c166bc6a9f74ccbfc7e839"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38461d02d66de17455072c9ba981d35f1d2a73024bee7790ac2f9e361ef1cd0c"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5a32087d720c608f42caed0ef36d2b3ea61a9d09ee59a5142d6070da9041b8f"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd65632acaf0d47608190a71bfe46b209719bf2beb59507db08ccdbe712f969b"}, + {file = "frozenlist-1.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261b9f5d17cac914531331ff1b1d452125bf5daa05faf73b71d935485b0c510b"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b89ac9768b82205936771f8d2eb3ce88503b1556324c9f903e7156669f521472"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:008eb8b31b3ea6896da16c38c1b136cb9fec9e249e77f6211d479db79a4eaf01"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e74b0506fa5aa5598ac6a975a12aa8928cbb58e1f5ac8360792ef15de1aa848f"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:490132667476f6781b4c9458298b0c1cddf237488abd228b0b3650e5ecba7467"}, + {file = "frozenlist-1.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:76d4711f6f6d08551a7e9ef28c722f4a50dd0fc204c56b4bcd95c6cc05ce6fbb"}, + {file = "frozenlist-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a02eb8ab2b8f200179b5f62b59757685ae9987996ae549ccf30f983f40602431"}, + {file = "frozenlist-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:515e1abc578dd3b275d6a5114030b1330ba044ffba03f94091842852f806f1c1"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0ed05f5079c708fe74bf9027e95125334b6978bf07fd5ab923e9e55e5fbb9d3"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ca265542ca427bf97aed183c1676e2a9c66942e822b14dc6e5f42e038f92a503"}, + {file = "frozenlist-1.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:491e014f5c43656da08958808588cc6c016847b4360e327a62cb308c791bd2d9"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17ae5cd0f333f94f2e03aaf140bb762c64783935cc764ff9c82dff626089bebf"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e78fb68cf9c1a6aa4a9a12e960a5c9dfbdb89b3695197aa7064705662515de2"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5655a942f5f5d2c9ed93d72148226d75369b4f6952680211972a33e59b1dfdc"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11b0746f5d946fecf750428a95f3e9ebe792c1ee3b1e96eeba145dc631a9672"}, + {file = "frozenlist-1.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e66d2a64d44d50d2543405fb183a21f76b3b5fd16f130f5c99187c3fb4e64919"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:88f7bc0fcca81f985f78dd0fa68d2c75abf8272b1f5c323ea4a01a4d7a614efc"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5833593c25ac59ede40ed4de6d67eb42928cca97f26feea219f21d0ed0959b79"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fec520865f42e5c7f050c2a79038897b1c7d1595e907a9e08e3353293ffc948e"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:b826d97e4276750beca7c8f0f1a4938892697a6bcd8ec8217b3312dad6982781"}, + {file = "frozenlist-1.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ceb6ec0a10c65540421e20ebd29083c50e6d1143278746a4ef6bcf6153171eb8"}, + {file = "frozenlist-1.4.0-cp38-cp38-win32.whl", hash = "sha256:2b8bcf994563466db019fab287ff390fffbfdb4f905fc77bc1c1d604b1c689cc"}, + {file = "frozenlist-1.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:a6c8097e01886188e5be3e6b14e94ab365f384736aa1fca6a0b9e35bd4a30bc7"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6c38721585f285203e4b4132a352eb3daa19121a035f3182e08e437cface44bf"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0c6da9aee33ff0b1a451e867da0c1f47408112b3391dd43133838339e410963"}, + {file = "frozenlist-1.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93ea75c050c5bb3d98016b4ba2497851eadf0ac154d88a67d7a6816206f6fa7f"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f61e2dc5ad442c52b4887f1fdc112f97caeff4d9e6ebe78879364ac59f1663e1"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa384489fefeb62321b238e64c07ef48398fe80f9e1e6afeff22e140e0850eef"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10ff5faaa22786315ef57097a279b833ecab1a0bfb07d604c9cbb1c4cdc2ed87"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:007df07a6e3eb3e33e9a1fe6a9db7af152bbd8a185f9aaa6ece10a3529e3e1c6"}, + {file = "frozenlist-1.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4f399d28478d1f604c2ff9119907af9726aed73680e5ed1ca634d377abb087"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5374b80521d3d3f2ec5572e05adc94601985cc526fb276d0c8574a6d749f1b3"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ce31ae3e19f3c902de379cf1323d90c649425b86de7bbdf82871b8a2a0615f3d"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7211ef110a9194b6042449431e08c4d80c0481e5891e58d429df5899690511c2"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:556de4430ce324c836789fa4560ca62d1591d2538b8ceb0b4f68fb7b2384a27a"}, + {file = "frozenlist-1.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7645a8e814a3ee34a89c4a372011dcd817964ce8cb273c8ed6119d706e9613e3"}, + {file = "frozenlist-1.4.0-cp39-cp39-win32.whl", hash = "sha256:19488c57c12d4e8095a922f328df3f179c820c212940a498623ed39160bc3c2f"}, + {file = "frozenlist-1.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:6221d84d463fb110bdd7619b69cb43878a11d51cbb9394ae3105d082d5199167"}, + {file = "frozenlist-1.4.0.tar.gz", hash = "sha256:09163bdf0b2907454042edb19f887c6d33806adc71fbd54afc14908bfdc22251"}, +] + +[[package]] +name = "idna" +version = "3.5" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.5-py3-none-any.whl", hash = "sha256:79b8f0ac92d2351be5f6122356c9a592c96d81c9a79e4b488bf2a6a15f88057a"}, + {file = "idna-3.5.tar.gz", hash = "sha256:27009fe2735bf8723353582d48575b23c533cc2c2de7b5a68908d91b5eb18d08"}, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "multidict" +version = "6.0.4" +description = "multidict implementation" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1a97283e0c85772d613878028fec909f003993e1007eafa715b24b377cb9b8"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb6dcc05e911516ae3d1f207d4b0520d07f54484c49dfc294d6e7d63b734171"}, + {file = "multidict-6.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d6d635d5209b82a3492508cf5b365f3446afb65ae7ebd755e70e18f287b0adf7"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c048099e4c9e9d615545e2001d3d8a4380bd403e1a0578734e0d31703d1b0c0b"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ea20853c6dbbb53ed34cb4d080382169b6f4554d394015f1bef35e881bf83547"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16d232d4e5396c2efbbf4f6d4df89bfa905eb0d4dc5b3549d872ab898451f569"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36c63aaa167f6c6b04ef2c85704e93af16c11d20de1d133e39de6a0e84582a93"}, + {file = "multidict-6.0.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64bdf1086b6043bf519869678f5f2757f473dee970d7abf6da91ec00acb9cb98"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:43644e38f42e3af682690876cff722d301ac585c5b9e1eacc013b7a3f7b696a0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7582a1d1030e15422262de9f58711774e02fa80df0d1578995c76214f6954988"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:ddff9c4e225a63a5afab9dd15590432c22e8057e1a9a13d28ed128ecf047bbdc"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:ee2a1ece51b9b9e7752e742cfb661d2a29e7bcdba2d27e66e28a99f1890e4fa0"}, + {file = "multidict-6.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a2e4369eb3d47d2034032a26c7a80fcb21a2cb22e1173d761a162f11e562caa5"}, + {file = "multidict-6.0.4-cp310-cp310-win32.whl", hash = "sha256:574b7eae1ab267e5f8285f0fe881f17efe4b98c39a40858247720935b893bba8"}, + {file = "multidict-6.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:4dcbb0906e38440fa3e325df2359ac6cb043df8e58c965bb45f4e406ecb162cc"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0dfad7a5a1e39c53ed00d2dd0c2e36aed4650936dc18fd9a1826a5ae1cad6f03"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:64da238a09d6039e3bd39bb3aee9c21a5e34f28bfa5aa22518581f910ff94af3"}, + {file = "multidict-6.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff959bee35038c4624250473988b24f846cbeb2c6639de3602c073f10410ceba"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01a3a55bd90018c9c080fbb0b9f4891db37d148a0a18722b42f94694f8b6d4c9"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5cb09abb18c1ea940fb99360ea0396f34d46566f157122c92dfa069d3e0e982"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:666daae833559deb2d609afa4490b85830ab0dfca811a98b70a205621a6109fe"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11bdf3f5e1518b24530b8241529d2050014c884cf18b6fc69c0c2b30ca248710"}, + {file = "multidict-6.0.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d18748f2d30f94f498e852c67d61261c643b349b9d2a581131725595c45ec6c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:458f37be2d9e4c95e2d8866a851663cbc76e865b78395090786f6cd9b3bbf4f4"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:b1a2eeedcead3a41694130495593a559a668f382eee0727352b9a41e1c45759a"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:7d6ae9d593ef8641544d6263c7fa6408cc90370c8cb2bbb65f8d43e5b0351d9c"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:5979b5632c3e3534e42ca6ff856bb24b2e3071b37861c2c727ce220d80eee9ed"}, + {file = "multidict-6.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dcfe792765fab89c365123c81046ad4103fcabbc4f56d1c1997e6715e8015461"}, + {file = "multidict-6.0.4-cp311-cp311-win32.whl", hash = "sha256:3601a3cece3819534b11d4efc1eb76047488fddd0c85a3948099d5da4d504636"}, + {file = "multidict-6.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:81a4f0b34bd92df3da93315c6a59034df95866014ac08535fc819f043bfd51f0"}, + {file = "multidict-6.0.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:67040058f37a2a51ed8ea8f6b0e6ee5bd78ca67f169ce6122f3e2ec80dfe9b78"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:853888594621e6604c978ce2a0444a1e6e70c8d253ab65ba11657659dcc9100f"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:39ff62e7d0f26c248b15e364517a72932a611a9b75f35b45be078d81bdb86603"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af048912e045a2dc732847d33821a9d84ba553f5c5f028adbd364dd4765092ac"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e8b901e607795ec06c9e42530788c45ac21ef3aaa11dbd0c69de543bfb79a9"}, + {file = "multidict-6.0.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62501642008a8b9871ddfccbf83e4222cf8ac0d5aeedf73da36153ef2ec222d2"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:99b76c052e9f1bc0721f7541e5e8c05db3941eb9ebe7b8553c625ef88d6eefde"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:509eac6cf09c794aa27bcacfd4d62c885cce62bef7b2c3e8b2e49d365b5003fe"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:21a12c4eb6ddc9952c415f24eef97e3e55ba3af61f67c7bc388dcdec1404a067"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:5cad9430ab3e2e4fa4a2ef4450f548768400a2ac635841bc2a56a2052cdbeb87"}, + {file = "multidict-6.0.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ab55edc2e84460694295f401215f4a58597f8f7c9466faec545093045476327d"}, + {file = "multidict-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:5a4dcf02b908c3b8b17a45fb0f15b695bf117a67b76b7ad18b73cf8e92608775"}, + {file = "multidict-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6ed5f161328b7df384d71b07317f4d8656434e34591f20552c7bcef27b0ab88e"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5fc1b16f586f049820c5c5b17bb4ee7583092fa0d1c4e28b5239181ff9532e0c"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1502e24330eb681bdaa3eb70d6358e818e8e8f908a22a1851dfd4e15bc2f8161"}, + {file = "multidict-6.0.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b692f419760c0e65d060959df05f2a531945af31fda0c8a3b3195d4efd06de11"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45e1ecb0379bfaab5eef059f50115b54571acfbe422a14f668fc8c27ba410e7e"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ddd3915998d93fbcd2566ddf9cf62cdb35c9e093075f862935573d265cf8f65d"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d43b61c59d82f2effb39a93c48b845efe23a3852d201ed2d24ba830d0b4cf2"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc8e1d0c705233c5dd0c5e6460fbad7827d5d36f310a0fadfd45cc3029762258"}, + {file = "multidict-6.0.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6aa0418fcc838522256761b3415822626f866758ee0bc6632c9486b179d0b52"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6748717bb10339c4760c1e63da040f5f29f5ed6e59d76daee30305894069a660"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:4d1a3d7ef5e96b1c9e92f973e43aa5e5b96c659c9bc3124acbbd81b0b9c8a951"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4372381634485bec7e46718edc71528024fcdc6f835baefe517b34a33c731d60"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:fc35cb4676846ef752816d5be2193a1e8367b4c1397b74a565a9d0389c433a1d"}, + {file = "multidict-6.0.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4b9d9e4e2b37daddb5c23ea33a3417901fa7c7b3dee2d855f63ee67a0b21e5b1"}, + {file = "multidict-6.0.4-cp38-cp38-win32.whl", hash = "sha256:e41b7e2b59679edfa309e8db64fdf22399eec4b0b24694e1b2104fb789207779"}, + {file = "multidict-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:d6c254ba6e45d8e72739281ebc46ea5eb5f101234f3ce171f0e9f5cc86991480"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16ab77bbeb596e14212e7bab8429f24c1579234a3a462105cda4a66904998664"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc779e9e6f7fda81b3f9aa58e3a6091d49ad528b11ed19f6621408806204ad35"}, + {file = "multidict-6.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ceef517eca3e03c1cceb22030a3e39cb399ac86bff4e426d4fc6ae49052cc60"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281af09f488903fde97923c7744bb001a9b23b039a909460d0f14edc7bf59706"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:52f2dffc8acaba9a2f27174c41c9e57f60b907bb9f096b36b1a1f3be71c6284d"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b41156839806aecb3641f3208c0dafd3ac7775b9c4c422d82ee2a45c34ba81ca"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3fc56f88cc98ef8139255cf8cd63eb2c586531e43310ff859d6bb3a6b51f1"}, + {file = "multidict-6.0.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8316a77808c501004802f9beebde51c9f857054a0c871bd6da8280e718444449"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f70b98cd94886b49d91170ef23ec5c0e8ebb6f242d734ed7ed677b24d50c82cf"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bf6774e60d67a9efe02b3616fee22441d86fab4c6d335f9d2051d19d90a40063"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e69924bfcdda39b722ef4d9aa762b2dd38e4632b3641b1d9a57ca9cd18f2f83a"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:6b181d8c23da913d4ff585afd1155a0e1194c0b50c54fcfe286f70cdaf2b7176"}, + {file = "multidict-6.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:52509b5be062d9eafc8170e53026fbc54cf3b32759a23d07fd935fb04fc22d95"}, + {file = "multidict-6.0.4-cp39-cp39-win32.whl", hash = "sha256:27c523fbfbdfd19c6867af7346332b62b586eed663887392cff78d614f9ec313"}, + {file = "multidict-6.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:33029f5734336aa0d4c0384525da0387ef89148dc7191aae00ca5fb23d7aafc2"}, + {file = "multidict-6.0.4.tar.gz", hash = "sha256:3666906492efb76453c0e7b97f2cf459b0682e7402c0489a95484965dbc1da49"}, +] + +[[package]] +name = "mypy" +version = "1.7.1" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12cce78e329838d70a204293e7b29af9faa3ab14899aec397798a4b41be7f340"}, + {file = "mypy-1.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1484b8fa2c10adf4474f016e09d7a159602f3239075c7bf9f1627f5acf40ad49"}, + {file = "mypy-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31902408f4bf54108bbfb2e35369877c01c95adc6192958684473658c322c8a5"}, + {file = "mypy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f2c2521a8e4d6d769e3234350ba7b65ff5d527137cdcde13ff4d99114b0c8e7d"}, + {file = "mypy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:fcd2572dd4519e8a6642b733cd3a8cfc1ef94bafd0c1ceed9c94fe736cb65b6a"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"}, + {file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"}, + {file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"}, + {file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"}, + {file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"}, + {file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"}, + {file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"}, + {file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"}, + {file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84860e06ba363d9c0eeabd45ac0fde4b903ad7aa4f93cd8b648385a888e23200"}, + {file = "mypy-1.7.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8c5091ebd294f7628eb25ea554852a52058ac81472c921150e3a61cdd68f75a7"}, + {file = "mypy-1.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40716d1f821b89838589e5b3106ebbc23636ffdef5abc31f7cd0266db936067e"}, + {file = "mypy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cf3f0c5ac72139797953bd50bc6c95ac13075e62dbfcc923571180bebb662e9"}, + {file = "mypy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:78e25b2fd6cbb55ddfb8058417df193f0129cad5f4ee75d1502248e588d9e0d7"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75c4d2a6effd015786c87774e04331b6da863fc3fc4e8adfc3b40aa55ab516fe"}, + {file = "mypy-1.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2643d145af5292ee956aa0a83c2ce1038a3bdb26e033dadeb2f7066fb0c9abce"}, + {file = "mypy-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75aa828610b67462ffe3057d4d8a4112105ed211596b750b53cbfe182f44777a"}, + {file = "mypy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ee5d62d28b854eb61889cde4e1dbc10fbaa5560cb39780c3995f6737f7e82120"}, + {file = "mypy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:72cf32ce7dd3562373f78bd751f73c96cfb441de147cc2448a92c1a308bd0ca6"}, + {file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"}, + {file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pathspec" +version = "0.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "4.0.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-4.0.0-py3-none-any.whl", hash = "sha256:118c954d7e949b35437270383a3f2531e99dd93cf7ce4dc8340d3356d30f173b"}, + {file = "platformdirs-4.0.0.tar.gz", hash = "sha256:cb633b2bcf10c51af60beb0ab06d2f1d69064b43abf4c185ca6b28865f3f9731"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[package]] +name = "pluggy" +version = "1.3.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.3.0-py3-none-any.whl", hash = "sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"}, + {file = "pluggy-1.3.0.tar.gz", hash = "sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "7.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, + {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.1.6" +description = "An extremely fast Python linter and code formatter, written in Rust." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:88b8cdf6abf98130991cbc9f6438f35f6e8d41a02622cc5ee130a02a0ed28703"}, + {file = "ruff-0.1.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c549ed437680b6105a1299d2cd30e4964211606eeb48a0ff7a93ef70b902248"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cf5f701062e294f2167e66d11b092bba7af6a057668ed618a9253e1e90cfd76"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:05991ee20d4ac4bb78385360c684e4b417edd971030ab12a4fbd075ff535050e"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87455a0c1f739b3c069e2f4c43b66479a54dea0276dd5d4d67b091265f6fd1dc"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:683aa5bdda5a48cb8266fcde8eea2a6af4e5700a392c56ea5fb5f0d4bfdc0240"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:137852105586dcbf80c1717facb6781555c4e99f520c9c827bd414fac67ddfb6"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd98138a98d48a1c36c394fd6b84cd943ac92a08278aa8ac8c0fdefcf7138f35"}, + {file = "ruff-0.1.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a0cd909d25f227ac5c36d4e7e681577275fb74ba3b11d288aff7ec47e3ae745"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8fd1c62a47aa88a02707b5dd20c5ff20d035d634aa74826b42a1da77861b5ff"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd89b45d374935829134a082617954120d7a1470a9f0ec0e7f3ead983edc48cc"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:491262006e92f825b145cd1e52948073c56560243b55fb3b4ecb142f6f0e9543"}, + {file = "ruff-0.1.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ea284789861b8b5ca9d5443591a92a397ac183d4351882ab52f6296b4fdd5462"}, + {file = "ruff-0.1.6-py3-none-win32.whl", hash = "sha256:1610e14750826dfc207ccbcdd7331b6bd285607d4181df9c1c6ae26646d6848a"}, + {file = "ruff-0.1.6-py3-none-win_amd64.whl", hash = "sha256:4558b3e178145491e9bc3b2ee3c4b42f19d19384eaa5c59d10acf6e8f8b57e33"}, + {file = "ruff-0.1.6-py3-none-win_arm64.whl", hash = "sha256:03910e81df0d8db0e30050725a5802441c2022ea3ae4fe0609b76081731accbc"}, + {file = "ruff-0.1.6.tar.gz", hash = "sha256:1b09f29b16c6ead5ea6b097ef2764b42372aebe363722f1605ecbcd2b9207184"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[[package]] +name = "yarl" +version = "1.9.3" +description = "Yet another URL library" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32435d134414e01d937cd9d6cc56e8413a8d4741dea36af5840c7750f04d16ab"}, + {file = "yarl-1.9.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9a5211de242754b5e612557bca701f39f8b1a9408dff73c6db623f22d20f470e"}, + {file = "yarl-1.9.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:525cd69eff44833b01f8ef39aa33a9cc53a99ff7f9d76a6ef6a9fb758f54d0ff"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc94441bcf9cb8c59f51f23193316afefbf3ff858460cb47b5758bf66a14d130"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e36021db54b8a0475805acc1d6c4bca5d9f52c3825ad29ae2d398a9d530ddb88"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0f17d1df951336a02afc8270c03c0c6e60d1f9996fcbd43a4ce6be81de0bd9d"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5f3faeb8100a43adf3e7925d556801d14b5816a0ac9e75e22948e787feec642"}, + {file = "yarl-1.9.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aed37db837ecb5962469fad448aaae0f0ee94ffce2062cf2eb9aed13328b5196"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:721ee3fc292f0d069a04016ef2c3a25595d48c5b8ddc6029be46f6158d129c92"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:b8bc5b87a65a4e64bc83385c05145ea901b613d0d3a434d434b55511b6ab0067"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:dd952b9c64f3b21aedd09b8fe958e4931864dba69926d8a90c90d36ac4e28c9a"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:c405d482c320a88ab53dcbd98d6d6f32ada074f2d965d6e9bf2d823158fa97de"}, + {file = "yarl-1.9.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9df9a0d4c5624790a0dea2e02e3b1b3c69aed14bcb8650e19606d9df3719e87d"}, + {file = "yarl-1.9.3-cp310-cp310-win32.whl", hash = "sha256:d34c4f80956227f2686ddea5b3585e109c2733e2d4ef12eb1b8b4e84f09a2ab6"}, + {file = "yarl-1.9.3-cp310-cp310-win_amd64.whl", hash = "sha256:cf7a4e8de7f1092829caef66fd90eaf3710bc5efd322a816d5677b7664893c93"}, + {file = "yarl-1.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d61a0ca95503867d4d627517bcfdc28a8468c3f1b0b06c626f30dd759d3999fd"}, + {file = "yarl-1.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73cc83f918b69110813a7d95024266072d987b903a623ecae673d1e71579d566"}, + {file = "yarl-1.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d81657b23e0edb84b37167e98aefb04ae16cbc5352770057893bd222cdc6e45f"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a1a8443091c7fbc17b84a0d9f38de34b8423b459fb853e6c8cdfab0eacf613"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe34befb8c765b8ce562f0200afda3578f8abb159c76de3ab354c80b72244c41"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c757f64afe53a422e45e3e399e1e3cf82b7a2f244796ce80d8ca53e16a49b9f"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72a57b41a0920b9a220125081c1e191b88a4cdec13bf9d0649e382a822705c65"}, + {file = "yarl-1.9.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:632c7aeb99df718765adf58eacb9acb9cbc555e075da849c1378ef4d18bf536a"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b0b8c06afcf2bac5a50b37f64efbde978b7f9dc88842ce9729c020dc71fae4ce"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1d93461e2cf76c4796355494f15ffcb50a3c198cc2d601ad8d6a96219a10c363"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:4003f380dac50328c85e85416aca6985536812c082387255c35292cb4b41707e"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4d6d74a97e898c1c2df80339aa423234ad9ea2052f66366cef1e80448798c13d"}, + {file = "yarl-1.9.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b61e64b06c3640feab73fa4ff9cb64bd8182de52e5dc13038e01cfe674ebc321"}, + {file = "yarl-1.9.3-cp311-cp311-win32.whl", hash = "sha256:29beac86f33d6c7ab1d79bd0213aa7aed2d2f555386856bb3056d5fdd9dab279"}, + {file = "yarl-1.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:f7271d6bd8838c49ba8ae647fc06469137e1c161a7ef97d778b72904d9b68696"}, + {file = "yarl-1.9.3-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:dd318e6b75ca80bff0b22b302f83a8ee41c62b8ac662ddb49f67ec97e799885d"}, + {file = "yarl-1.9.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c4b1efb11a8acd13246ffb0bee888dd0e8eb057f8bf30112e3e21e421eb82d4a"}, + {file = "yarl-1.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f034386e5550b5dc8ded90b5e2ff7db21f0f5c7de37b6efc5dac046eb19c10"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cd49a908cb6d387fc26acee8b7d9fcc9bbf8e1aca890c0b2fdfd706057546080"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa4643635f26052401750bd54db911b6342eb1a9ac3e74f0f8b58a25d61dfe41"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e741bd48e6a417bdfbae02e088f60018286d6c141639359fb8df017a3b69415a"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c86d0d0919952d05df880a1889a4f0aeb6868e98961c090e335671dea5c0361"}, + {file = "yarl-1.9.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d5434b34100b504aabae75f0622ebb85defffe7b64ad8f52b8b30ec6ef6e4b9"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79e1df60f7c2b148722fb6cafebffe1acd95fd8b5fd77795f56247edaf326752"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:44e91a669c43f03964f672c5a234ae0d7a4d49c9b85d1baa93dec28afa28ffbd"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:3cfa4dbe17b2e6fca1414e9c3bcc216f6930cb18ea7646e7d0d52792ac196808"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:88d2c3cc4b2f46d1ba73d81c51ec0e486f59cc51165ea4f789677f91a303a9a7"}, + {file = "yarl-1.9.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cccdc02e46d2bd7cb5f38f8cc3d9db0d24951abd082b2f242c9e9f59c0ab2af3"}, + {file = "yarl-1.9.3-cp312-cp312-win32.whl", hash = "sha256:96758e56dceb8a70f8a5cff1e452daaeff07d1cc9f11e9b0c951330f0a2396a7"}, + {file = "yarl-1.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:c4472fe53ebf541113e533971bd8c32728debc4c6d8cc177f2bff31d011ec17e"}, + {file = "yarl-1.9.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:126638ab961633f0940a06e1c9d59919003ef212a15869708dcb7305f91a6732"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c99ddaddb2fbe04953b84d1651149a0d85214780e4d0ee824e610ab549d98d92"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dab30b21bd6fb17c3f4684868c7e6a9e8468078db00f599fb1c14e324b10fca"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:828235a2a169160ee73a2fcfb8a000709edf09d7511fccf203465c3d5acc59e4"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc391e3941045fd0987c77484b2799adffd08e4b6735c4ee5f054366a2e1551d"}, + {file = "yarl-1.9.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:51382c72dd5377861b573bd55dcf680df54cea84147c8648b15ac507fbef984d"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:28a108cb92ce6cf867690a962372996ca332d8cda0210c5ad487fe996e76b8bb"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8f18a7832ff85dfcd77871fe677b169b1bc60c021978c90c3bb14f727596e0ae"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:7eaf13af79950142ab2bbb8362f8d8d935be9aaf8df1df89c86c3231e4ff238a"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:66a6dbf6ca7d2db03cc61cafe1ee6be838ce0fbc97781881a22a58a7c5efef42"}, + {file = "yarl-1.9.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:1a0a4f3aaa18580038cfa52a7183c8ffbbe7d727fe581300817efc1e96d1b0e9"}, + {file = "yarl-1.9.3-cp37-cp37m-win32.whl", hash = "sha256:946db4511b2d815979d733ac6a961f47e20a29c297be0d55b6d4b77ee4b298f6"}, + {file = "yarl-1.9.3-cp37-cp37m-win_amd64.whl", hash = "sha256:2dad8166d41ebd1f76ce107cf6a31e39801aee3844a54a90af23278b072f1ccf"}, + {file = "yarl-1.9.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:bb72d2a94481e7dc7a0c522673db288f31849800d6ce2435317376a345728225"}, + {file = "yarl-1.9.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9a172c3d5447b7da1680a1a2d6ecdf6f87a319d21d52729f45ec938a7006d5d8"}, + {file = "yarl-1.9.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2dc72e891672343b99db6d497024bf8b985537ad6c393359dc5227ef653b2f17"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8d51817cf4b8d545963ec65ff06c1b92e5765aa98831678d0e2240b6e9fd281"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53ec65f7eee8655bebb1f6f1607760d123c3c115a324b443df4f916383482a67"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cfd77e8e5cafba3fb584e0f4b935a59216f352b73d4987be3af51f43a862c403"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e73db54c967eb75037c178a54445c5a4e7461b5203b27c45ef656a81787c0c1b"}, + {file = "yarl-1.9.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09c19e5f4404574fcfb736efecf75844ffe8610606f3fccc35a1515b8b6712c4"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6280353940f7e5e2efaaabd686193e61351e966cc02f401761c4d87f48c89ea4"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c25ec06e4241e162f5d1f57c370f4078797ade95c9208bd0c60f484834f09c96"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7217234b10c64b52cc39a8d82550342ae2e45be34f5bff02b890b8c452eb48d7"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4ce77d289f8d40905c054b63f29851ecbfd026ef4ba5c371a158cfe6f623663e"}, + {file = "yarl-1.9.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5f74b015c99a5eac5ae589de27a1201418a5d9d460e89ccb3366015c6153e60a"}, + {file = "yarl-1.9.3-cp38-cp38-win32.whl", hash = "sha256:8a2538806be846ea25e90c28786136932ec385c7ff3bc1148e45125984783dc6"}, + {file = "yarl-1.9.3-cp38-cp38-win_amd64.whl", hash = "sha256:6465d36381af057d0fab4e0f24ef0e80ba61f03fe43e6eeccbe0056e74aadc70"}, + {file = "yarl-1.9.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2f3c8822bc8fb4a347a192dd6a28a25d7f0ea3262e826d7d4ef9cc99cd06d07e"}, + {file = "yarl-1.9.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7831566595fe88ba17ea80e4b61c0eb599f84c85acaa14bf04dd90319a45b90"}, + {file = "yarl-1.9.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ff34cb09a332832d1cf38acd0f604c068665192c6107a439a92abfd8acf90fe2"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe8080b4f25dfc44a86bedd14bc4f9d469dfc6456e6f3c5d9077e81a5fedfba7"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8535e111a064f3bdd94c0ed443105934d6f005adad68dd13ce50a488a0ad1bf3"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d155a092bf0ebf4a9f6f3b7a650dc5d9a5bbb585ef83a52ed36ba46f55cc39d"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:778df71c8d0c8c9f1b378624b26431ca80041660d7be7c3f724b2c7a6e65d0d6"}, + {file = "yarl-1.9.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f9cafaf031c34d95c1528c16b2fa07b710e6056b3c4e2e34e9317072da5d1a"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ca6b66f69e30f6e180d52f14d91ac854b8119553b524e0e28d5291a724f0f423"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e0e7e83f31e23c5d00ff618045ddc5e916f9e613d33c5a5823bc0b0a0feb522f"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:af52725c7c39b0ee655befbbab5b9a1b209e01bb39128dce0db226a10014aacc"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0ab5baaea8450f4a3e241ef17e3d129b2143e38a685036b075976b9c415ea3eb"}, + {file = "yarl-1.9.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d350388ba1129bc867c6af1cd17da2b197dff0d2801036d2d7d83c2d771a682"}, + {file = "yarl-1.9.3-cp39-cp39-win32.whl", hash = "sha256:e2a16ef5fa2382af83bef4a18c1b3bcb4284c4732906aa69422cf09df9c59f1f"}, + {file = "yarl-1.9.3-cp39-cp39-win_amd64.whl", hash = "sha256:d92d897cb4b4bf915fbeb5e604c7911021a8456f0964f3b8ebbe7f9188b9eabb"}, + {file = "yarl-1.9.3-py3-none-any.whl", hash = "sha256:271d63396460b6607b588555ea27a1a02b717ca2e3f2cf53bdde4013d7790929"}, + {file = "yarl-1.9.3.tar.gz", hash = "sha256:4a14907b597ec55740f63e52d7fee0e9ee09d5b9d57a4f399a7423268e457b57"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "bd8086003744182c0b34aa7331d0c43dc32a86f0772d0ac5b86ac96f069be80d" diff --git a/poetry.toml b/poetry.toml new file mode 100644 index 0000000..ab1033b --- /dev/null +++ b/poetry.toml @@ -0,0 +1,2 @@ +[virtualenvs] +in-project = true diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..07431a1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[tool.poetry] +name = "evmole" +version = "0.0.1" +description = "Extracts function selectors from EVM bytecode" +authors = ["Maxim Andreev "] +license = "MIT" +readme = "README.md" +repository = "https://github.com/cdump/evmole" + +[tool.poetry.dependencies] +python = "^3.10" +aiohttp = {version = "^3.9.0", extras = ["benchmark"]} + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +black = "^23.11.0" +mypy = "^1.7.1" +ruff = "^0.1.6" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 130 +target-version = ["py311"] +skip-string-normalization = true + +[tool.ruff] +extend-select = ["B", "Q"] +line-length = 130 +target-version = "py311" + +[tool.ruff.flake8-quotes] +inline-quotes = "single" + +[tool.ruff.per-file-ignores] +"__init__.py" = ["F401"] diff --git a/tests/memory_test.py b/tests/memory_test.py new file mode 100644 index 0000000..f24dff7 --- /dev/null +++ b/tests/memory_test.py @@ -0,0 +1,14 @@ +from evmole.evm.memory import Memory + +def test_memory(): + m = Memory() + + m.store(0, b'\xaa') + assert m.load(0) == (b'\xaa' + b'\x00'*31, [b'\xaa']) + assert m.load(1) == (b'\x00'*32, []) + + m.store(8, b'\xbb') + assert m.load(0) == (b'\xaa' + b'\x00'*7 + b'\xbb' + b'\x00'*23, [b'\xaa', b'\xbb']) + assert m.load(2) == (b'\x00'*6 + b'\xbb' + b'\x00'*25, [b'\xbb']) + assert m.load(8) == (b'\xbb' + b'\x00'*31, [b'\xbb']) + assert m.load(9) == (b'\x00'*32, []) diff --git a/tests/stack_test.py b/tests/stack_test.py new file mode 100644 index 0000000..0ddab2c --- /dev/null +++ b/tests/stack_test.py @@ -0,0 +1,16 @@ +from evmole.evm.stack import Stack + +def test_stack(): + s = Stack() + s.push(b'\xaa') + assert s.pop() == b'\xaa' + + s.push(b'\xaa') + s.push(b'\xbb') + assert s.pop() == b'\xbb' + assert s.pop() == b'\xaa' + + s.push(b'\xaa') + assert s.peek() == b'\xaa' + assert s.pop() == b'\xaa' + assert s.peek() is None