Merge pull request #97 from zomglings/docstrings

Docstrings
async-crawl-events v0.5.0
Yhtyyar Sahatov 2022-09-20 14:04:41 +03:00 zatwierdzone przez GitHub
commit 18cb7e55e9
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
8 zmienionych plików z 187 dodań i 555 usunięć

Wyświetl plik

@ -23,11 +23,17 @@ from .version import MOONWORM_VERSION
def write_file(content: str, path: str):
"""
Write content to filesystem at the specified path.
"""
with open(path, "w") as ofp:
ofp.write(content)
def copy_web3_util(dest_dir: str, force: bool = False) -> None:
"""
Copy the web3_util.py file to the given destination directory.
"""
dest_filepath = os.path.join(dest_dir, "web3_util.py")
if os.path.isfile(dest_filepath) and not force:
print(f"{dest_filepath} file already exists. Use -f to rewrite")
@ -36,6 +42,9 @@ def copy_web3_util(dest_dir: str, force: bool = False) -> None:
def create_init_py(dest_dir: str, force: bool = False) -> None:
"""
Create __init__.py file in destination directory.
"""
dest_filepath = os.path.join(dest_dir, "__init__.py")
if os.path.isfile(dest_filepath) and not force:
print(f"{dest_filepath} file already exists. Use -f to rewrite")
@ -44,6 +53,10 @@ def create_init_py(dest_dir: str, force: bool = False) -> None:
def handle_generate(args: argparse.Namespace) -> None:
"""
Handler for the "moonworm generate" command, which generates web3.py-compatible interfaces to a
given smart contract.
"""
if not args.interface and not args.cli:
print("Please specify what you want to generate:")
print("--interface for smart contract interface")
@ -85,7 +98,10 @@ def handle_generate(args: argparse.Namespace) -> None:
def handle_brownie_generate(args: argparse.Namespace):
"""
Handler for the "moonworm generate-brownie" command, which generates brownie-compatible interfaces
to a given smart contract.
"""
Path(args.outdir).mkdir(exist_ok=True)
project_directory = args.project
@ -118,6 +134,10 @@ def handle_brownie_generate(args: argparse.Namespace):
def handle_watch(args: argparse.Namespace) -> None:
"""
Handler for the "moonworm watch" command, which records all events and transactions against a given
smart contract between the specified block range.
"""
if args.abi == "erc20":
contract_abi = ERC20.abi()
elif args.abi == "erc721":
@ -177,35 +197,11 @@ def handle_watch(args: argparse.Namespace) -> None:
)
def handle_watch_cu(args: argparse.Namespace) -> None:
from moonworm.cu_watch import watch_cu_contract
MOONSTREAM_DB_URI = os.environ.get("MOONSTREAM_DB_URI")
if not MOONSTREAM_DB_URI:
print("Please set MOONSTREAM_DB_URI environment variable")
return
if args.abi is not None:
with open(args.abi, "r") as ifp:
contract_abi = json.load(ifp)
else:
print("Using CUContract abi since no abi is specified")
contract_abi = CU.abi()
web3 = Web3(Web3.HTTPProvider(args.web3))
web3.middleware_onion.inject(geth_poa_middleware, layer=0)
watch_cu_contract(
web3,
web3.toChecksumAddress(args.contract),
contract_abi,
args.confirmations,
start_block=args.deployment_block,
force_start=args.force,
)
def handle_find_deployment(args: argparse.Namespace) -> None:
"""
Handler for the "moonworm find-deployment" command, which finds the deployment block for a given
smart contract.
"""
web3_client = Web3(Web3.HTTPProvider(args.web3))
result = find_deployment_block(web3_client, args.contract, args.interval)
if result is None:
@ -216,6 +212,9 @@ def handle_find_deployment(args: argparse.Namespace) -> None:
def generate_argument_parser() -> argparse.ArgumentParser:
"""
Generates the command-line argument parser for the "moonworm" command.
"""
parser = argparse.ArgumentParser(description="Moonworm: Manage your smart contract")
parser.add_argument(
"-v",
@ -327,51 +326,6 @@ def generate_argument_parser() -> argparse.ArgumentParser:
watch_parser.set_defaults(func=handle_watch)
watch_cu_parser = subcommands.add_parser(
"watch-cu", help="Watch a Crypto Unicorns contract"
)
watch_cu_parser.add_argument(
"-i",
"--abi",
default=None,
help="ABI file path, default is abi in fixtures/abis/CU.json",
)
watch_cu_parser.add_argument(
"-f",
"--force",
action="store_true",
help="Force start from given block",
)
watch_cu_parser.add_argument(
"-c",
"--contract",
required=True,
help="Contract address",
)
watch_cu_parser.add_argument(
"-w",
"--web3",
required=True,
help="Web3 provider",
)
watch_cu_parser.add_argument(
"--confirmations",
default=10,
type=int,
help="Number of confirmations to wait for. Default=12",
)
watch_cu_parser.add_argument(
"--deployment-block",
"-d",
type=int,
help="Block number of the deployment",
)
watch_cu_parser.set_defaults(func=handle_watch_cu)
generate_brownie_parser = subcommands.add_parser(
"generate-brownie", description="Moonworm code generator for brownie projects"
)
@ -471,6 +425,9 @@ def generate_argument_parser() -> argparse.ArgumentParser:
def main() -> None:
"""
Handler for the "moonworm" command.
"""
parser = generate_argument_parser()
args = parser.parse_args()
args.func(args)

Wyświetl plik

@ -1,130 +0,0 @@
import json
from dataclasses import dataclass
from re import L
from moonstreamdb.db import yield_db_session_ctx
from moonstreamdb.models import PolygonLabel
from sqlalchemy.orm.session import Session
from web3 import Web3
from .contracts import CU
ADDRESS = "0xdC0479CC5BbA033B3e7De9F178607150B3AbCe1f"
MUMBAI_ADDRESS = "0xA993c4759B731f650dfA011765a6aedaC91a4a88"
def get_all_DNA_events(session: Session):
labels = (
session.query(PolygonLabel)
.filter(PolygonLabel.label == "moonworm")
.filter(PolygonLabel.address == ADDRESS)
.filter(PolygonLabel.label_data["name"].astext == "DNAUpdated")
.all()
)
max_token = -1
token_ids = []
for label in labels:
token_ids.append(label.label_data["args"]["tokenId"])
max_token = max(max_token, label.label_data["args"]["tokenId"])
print(len(token_ids))
return token_ids
def filter_ids(_id, labeled_ids):
with open(f"unicorn-classes-{_id}.json", "r") as ifp:
original_ids = json.load(ifp)
token_ids = [item["tokenId"] for item in original_ids]
result = []
for token in token_ids:
if token not in labeled_ids:
result.append({"tokenId": token, "class": _id})
with open(f"processes-{_id}.json", "w") as ofp:
json.dump(result, ofp)
action_map = {
0: "add",
1: "replace",
2: "remove",
}
cu_contract = Web3().eth.contract(abi=CU.abi())
def get_all_diamond_cuts(session: Session):
labels = (
session.query(PolygonLabel.label_data)
.filter(PolygonLabel.label == "moonworm")
.filter(PolygonLabel.address == ADDRESS)
.filter(PolygonLabel.label_data["name"].astext == "diamondCut")
.filter(PolygonLabel.label_data["status"].astext == "1")
.order_by(PolygonLabel.block_number.asc())
.all()
)
return labels
def get_function_name_by_selector(selector: str):
try:
name = cu_contract.get_function_by_selector(selector).function_identifier
return name
except Exception as e:
print(e)
print(selector)
return "UNKNOWN"
def run():
with yield_db_session_ctx() as session:
diamond_cuts_events = get_all_diamond_cuts(session)
selector_actions = {}
current_index = 0
for event in diamond_cuts_events:
diamond_cuts = event[0]["args"]["_diamondCut"]
for diamond_cut in diamond_cuts:
for selector in diamond_cut[2]:
if selector not in selector_actions:
selector_actions[selector] = []
selector_actions[selector].append(
{
"name": get_function_name_by_selector(selector),
"action": action_map[diamond_cut[1]],
"address": diamond_cut[0],
"action_index": current_index,
}
)
current_index += 1
current_cuts = {}
for selector, actions in selector_actions.items():
last_cut = None
last_cut_index = -1
for action in actions:
if action["action_index"] > last_cut_index:
last_cut_index = action["action_index"]
if action["action"] == "remove":
last_cut = {"name": action["name"], "address": None}
else:
last_cut = {"name": action["name"], "address": action["address"]}
current_cuts[selector] = last_cut
grouped_by_name = {}
for selector, cut in current_cuts.items():
if grouped_by_name.get(cut["name"]) is None:
grouped_by_name[cut["name"]] = []
grouped_by_name[cut["name"]].append(
{"selector": selector, "address": cut["address"]}
)
with open("current_cuts.json", "w") as ofp:
json.dump(grouped_by_name, ofp)
run()

Wyświetl plik

@ -1,344 +0,0 @@
import json
import logging
import os
import pprint as pp
import re
import time
from os import stat
from re import S
from typing import Any, Dict, List, Optional, cast
import web3
from eth_typing.evm import ChecksumAddress
from moonstreamdb.db import yield_db_session_ctx
from moonstreamdb.models import EthereumLabel, PolygonLabel
from sqlalchemy.orm import Query, Session
from sqlalchemy.sql.expression import delete
from tqdm import tqdm
from web3 import Web3
from web3.middleware import geth_poa_middleware
from moonworm.crawler.moonstream_ethereum_state_provider import (
MoonstreamEthereumStateProvider,
)
from moonworm.crawler.utils import Network
from .contracts import CU, ERC721
from .crawler.ethereum_state_provider import EthereumStateProvider
from .crawler.function_call_crawler import (
ContractFunctionCall,
FunctionCallCrawler,
FunctionCallCrawlerState,
Web3StateProvider,
utfy_dict,
)
from .crawler.log_scanner import _fetch_events_chunk
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def _get_last_crawled_block(
session: Session, contract_address: ChecksumAddress
) -> Optional[int]:
"""
Gets the last block that was crawled.
"""
query = (
session.query(PolygonLabel)
.filter(
PolygonLabel.label == "moonworm",
PolygonLabel.address == contract_address,
)
.order_by(PolygonLabel.block_number.desc())
)
if query.count():
return query.first().block_number
return None
def _add_function_call_labels(
session: Session,
function_calls: List[ContractFunctionCall],
address: ChecksumAddress,
) -> None:
"""
Adds a label to a function call.
"""
existing_function_call_labels = (
session.query(PolygonLabel)
.filter(
PolygonLabel.label == "moonworm",
PolygonLabel.log_index == None,
PolygonLabel.address == address,
PolygonLabel.transaction_hash.in_(
[call.transaction_hash for call in function_calls]
),
)
.all()
)
# deletin existing labels
for label in existing_function_call_labels:
session.delete(label)
try:
if existing_function_call_labels:
logger.info(
f"Deleting {len(existing_function_call_labels)} existing tx labels"
)
session.commit()
except Exception as e:
try:
session.commit()
except:
logger.error(f"Failed!!!\n{e}")
session.rollback()
for function_call in function_calls:
label = PolygonLabel(
label="moonworm",
label_data={
"type": "tx_call",
"name": function_call.function_name,
"caller": function_call.caller_address,
"args": function_call.function_args,
"status": function_call.status,
"gasUsed": function_call.gas_used,
},
address=function_call.contract_address,
block_number=function_call.block_number,
transaction_hash=function_call.transaction_hash,
block_timestamp=function_call.block_timestamp,
)
session.add(label)
try:
session.commit()
except Exception as e:
try:
session.commit()
except:
logger.error(f"Failed!!!\n{e}")
session.rollback()
def _add_event_labels(
session: Session, events: List[Dict[str, Any]], address: ChecksumAddress
) -> None:
"""
Adds events to database.
"""
transactions = [event["transactionHash"] for event in events]
log_indexes = [event["logIndex"] for event in events]
for ev in events:
print(ev)
existing_event_labels = (
session.query(PolygonLabel)
.filter(
PolygonLabel.label == "moonworm",
PolygonLabel.address == address,
PolygonLabel.transaction_hash.in_(transactions),
PolygonLabel.log_index != None,
)
.all()
)
# deletin existing labels
deleted = 0
for label in existing_event_labels:
if label.log_index in log_indexes:
deleted += 1
session.delete(label)
try:
if deleted > 0:
logger.error(f"Deleting {deleted} existing event labels")
session.commit()
except Exception as e:
try:
session.commit()
except:
logger.error(f"Failed!!!\n{e}")
session.rollback()
for event in events:
label = PolygonLabel(
label="moonworm",
label_data={
"type": "event",
"name": event["event"],
"args": event["args"],
},
address=event["address"],
block_number=event["blockNumber"],
transaction_hash=event["transactionHash"],
block_timestamp=event["blockTimestamp"],
log_index=event["logIndex"],
)
session.add(label)
try:
session.commit()
except Exception as e:
try:
session.commit()
except:
logger.error(f"Failed!!!\n{e}")
session.rollback()
class MockState(FunctionCallCrawlerState):
def __init__(self) -> None:
self.state: List[ContractFunctionCall] = []
def get_last_crawled_block(self) -> int:
"""
Returns the last block number that was crawled.
"""
return 0
def register_call(self, function_call: ContractFunctionCall) -> None:
"""
Processes the given function call (store it, etc.).
"""
self.state.append(function_call)
def flush(self) -> None:
"""
Flushes cached state to storage layer.
"""
self.state = []
def watch_cu_contract(
web3: Web3,
contract_address: ChecksumAddress,
contract_abi: List[Dict[str, Any]],
num_confirmations: int = 60,
min_blocks_to_crawl: int = 10,
sleep_time: float = 1,
start_block: Optional[int] = None,
force_start: bool = False,
use_moonstream_web3_provider: bool = False,
) -> None:
"""
Watches a contract for events and calls.
"""
if force_start and start_block is None:
raise ValueError("start_block must be specified if force_start is True")
with yield_db_session_ctx() as session:
function_call_state = MockState()
eth_state_provider: Optional[EthereumStateProvider] = None
if use_moonstream_web3_provider:
eth_state_provider = MoonstreamEthereumStateProvider(
web3, network=Network.polygon, db_session=session
)
else:
eth_state_provider = Web3StateProvider(web3)
crawler = FunctionCallCrawler(
function_call_state,
eth_state_provider,
contract_abi,
[web3.toChecksumAddress(contract_address)],
)
last_crawled_block = _get_last_crawled_block(session, contract_address)
if start_block is None:
if last_crawled_block is not None:
current_block = last_crawled_block
logger.info(f"Starting from block {current_block}, last crawled block")
else:
current_block = web3.eth.blockNumber - num_confirmations * 2
logger.info(f"Starting from block {current_block}, current block")
else:
current_block = start_block
if not force_start and last_crawled_block is not None:
if start_block > last_crawled_block:
logger.info(
f"Starting from block {start_block}, last crawled block {last_crawled_block}"
)
else:
current_block = last_crawled_block
logger.info(f"Starting from last crawled block {current_block}")
event_abis = [item for item in contract_abi if item["type"] == "event"]
while True:
try:
session.execute("select 1")
time.sleep(sleep_time)
end_block = min(
web3.eth.blockNumber - num_confirmations, current_block + 100
)
if end_block < current_block + min_blocks_to_crawl:
logger.info(
f"Sleeping crawling, end_block {end_block} < current_block {current_block} + min_blocks_to_crawl {min_blocks_to_crawl}"
)
sleep_time += 1
continue
sleep_time /= 2
logger.info("Getting txs")
crawler.crawl(current_block, end_block)
if function_call_state.state:
_add_function_call_labels(
session, function_call_state.state, contract_address
)
logger.info(
f"Got {len(function_call_state.state)} transaction calls:"
)
function_call_state.flush()
logger.info("Getting events")
all_events = []
for event_abi in event_abis:
raw_events = _fetch_events_chunk(
web3,
event_abi,
current_block,
end_block,
[contract_address],
)
for raw_event in raw_events:
raw_event["blockTimestamp"] = (
crawler.ethereum_state_provider.get_block_timestamp(
raw_event["blockNumber"]
),
)
all_events.append(raw_event)
if all_events:
print(f"Got {len(all_events)} events:")
_add_event_labels(session, all_events, contract_address)
logger.info(f"Current block {end_block + 1}")
current_block = end_block + 1
except Exception as e:
logger.error(f"Something went wrong: {e}")
logger.info(f"Trying to recover from error")
for i in range(10):
logger.info(f"Attempt {i}:")
try:
time.sleep(10)
logger.info("Trying to reconnect to database")
session.rollback()
session.execute("select 1")
logger.info("Trying to reconnect to web3")
web3.eth.block_number
break
except Exception as e:
logger.error(f"Failed: {e}")
continue
try:
session.execute("select 1")
web3.eth.block_number
continue
except Exception as e:
logger.error("Moonworm is going to die")
raise e

Wyświetl plik

@ -1,5 +1,7 @@
"""
This module allows users to inspect the conditions under which a smart contract was deployed.
Allows users to inspect the conditions under which a smart contract was deployed.
The entrypoint for this functionality is [`find_deployment_block`][moonworm.deployment.find_deployment_block].
"""
import logging
import os
@ -53,10 +55,27 @@ def find_deployment_block(
web3_interval: float,
) -> Optional[int]:
"""
Note: We will assume no selfdestruct for now.
Performs a binary search on the blockchain to discover precisely the block when a smart contract was
deployed.
This means that, if the address does not currently contain code, we will assume it never contained
code and is therefore not a smart contract address.
Note: Assumes no selfdestruct. This means that, if the address does not currently contain code,
we will assume it never contained code and is therefore not a smart contract address.
## Inputs
1. `web3_client`: A web3 client through which we can get block and address information on the blockchain.
An instance of web3.Web3.
2. `contract_address`: Address of the smart contract for which we want the deployment block. If this
address does not represent a smart contract, this method will return None.
3. `web3_interval`: Number of seconds to wait between requests to the web3_client. Useful if your
web3 provider rate limits you.
## Outputs
Returns the block number of the block in which the smart contract was deployed. If the address does
not represent an existing smart contract, returns None.
"""
log_prefix = f"find_deployment_block(web3_client, contract_address={contract_address}, web3_interval={web3_interval}) -- "

Wyświetl plik

@ -1,4 +1,12 @@
import copy
"""
Generates [`web3.py`](https://github.com/ethereum/web3.py)-compatible bindings to Ethereum smart contracts from their ABIs.
The entrypoints to code generation are:
- [`generate_contract_interface_content`][moonworm.generators.basic.generate_contract_interface_content]
- [`generate_contract_cli_content`][moonworm.generators.basic.generate_contract_cli_content]
"""
import keyword
import logging
import os
@ -479,6 +487,19 @@ def generate_argument_parser_function(abi: List[Dict[str, Any]]) -> cst.Function
def generate_contract_interface_content(
abi: List[Dict[str, Any]], abi_file_name: str, format: bool = True
) -> str:
"""
Generates a Python class designed to interact with a smart contract with the given ABI.
## Inputs
1. `abi`: The ABI to the smart contract. This is expected to be the Python representation of a JSON
list that conforms to the [Solidity ABI specification](https://docs.soliditylang.org/en/v0.8.16/abi-spec.html#json).
2. `abi_file_name`: Path where the contract ABI will be stored as part of codegen.
3. `format`: If True, uses [`black`](https://github.com/psf/black) to format the generated code before
returning it.
## Outputs
The generated code as a string.
"""
contract_body = cst.Module(body=[generate_contract_class(abi)]).code
content = INTERFACE_FILE_TEMPLATE.format(
@ -496,6 +517,19 @@ def generate_contract_interface_content(
def generate_contract_cli_content(
abi: List[Dict[str, Any]], abi_file_name: str, format: bool = True
) -> str:
"""
Generates a command-line interface designed to interact with a smart contract with the given ABI.
## Inputs
1. `abi`: The ABI to the smart contract. This is expected to be the Python representation of a JSON
list that conforms to the [Solidity ABI specification](https://docs.soliditylang.org/en/v0.8.16/abi-spec.html#json).
2. `abi_file_name`: Path where the contract ABI will be stored as part of codegen.
3. `format`: If True, uses [`black`](https://github.com/psf/black) to format the generated code before
returning it.
## Outputs
The generated code as a string.
"""
cli_body = cst.Module(body=[generate_argument_parser_function(abi)]).code
content = CLI_FILE_TEMPLATE.format(

Wyświetl plik

@ -1,3 +1,9 @@
"""
Generates [`brownie`](https://github.com/eth-brownie/brownie)-compatible bindings to Ethereum smart contracts from their ABIs.
The entrypoint to code generation is [`generate_brownie_interface`][moonworm.generators.brownie.generate_brownie_interface].
"""
import copy
import logging
import os
@ -865,6 +871,17 @@ def generate_brownie_cli(
) -> List[cst.FunctionDef]:
"""
Generates an argparse CLI to a brownie smart contract using the generated smart contract interface.
## Inputs
1. `abi`: The ABI to the smart contract. This is expected to be the Python representation of a JSON
list that conforms to the [Solidity ABI specification](https://docs.soliditylang.org/en/v0.8.16/abi-spec.html#json).
2. `contract_name`: Name for the smart contract
## Outputs
Concrete syntax tree representation of the generated code.
"""
get_transaction_config_function = generate_get_transaction_config()
add_default_arguments_function = generate_add_default_arguments()
@ -900,6 +917,35 @@ def generate_brownie_interface(
format: bool = True,
prod: bool = False,
) -> str:
"""
Generates Python code which allows you to interact with a smart contract with a given ABI, build data, and a given name.
The generated code uses the [`brownie`](https://github.com/eth-brownie/brownie) tool to interact with
the blockchain.
## Inputs
1. `abi`: The ABI to the smart contract. This is expected to be the Python representation of a JSON
list that conforms to the [Solidity ABI specification](https://docs.soliditylang.org/en/v0.8.16/abi-spec.html#json).
2. `contract_build`: `brownie` build information for the contract (e.g. its bytecode).
3. `contract_name`: Name for the smart contract
4. `relative_path`: Path to brownie project directory (relative to target path for the generated code).
5. `cli`: Set to True if a CLI should be generated in addition to a Python class.
6. `format`: If True, uses [`black`](https://github.com/psf/black) to format the generated code before
returning it.
7. `prod`: If True, creates a self-contained file. Generated code will not require reference to an
existing brownie project at its runtime.
## Outputs
The generated code as a string.
"""
contract_class = generate_brownie_contract_class(abi, contract_name)
module_body = [contract_class]

Wyświetl plik

@ -1 +1 @@
MOONWORM_VERSION = "0.4.0"
MOONWORM_VERSION = "0.5.0"

Wyświetl plik

@ -1,3 +1,10 @@
"""
Implements the moonworm smart contract crawler.
The [`watch_contract`][moonworm.watch.watch_contract] method is the entrypoint to this functionality
and it is what powers the "moonworm watch" command.
"""
import json
import pprint as pp
import time
@ -60,7 +67,50 @@ def watch_contract(
outfile: Optional[str] = None,
) -> None:
"""
Watches a contract for events and calls.
Watches a contract for events and method calls.
Currently supports crawling events and direct method calls on a smart contract.
It does *not* currently support crawling internal messages to a smart contract - this means that any
calls made to the target smart contract from *another* smart contract will not be recorded directly
in the crawldata. If the internal message resulted in any events being emitted on the target
contract, those events *will* be reflected in the crawldata.
## Inputs
1. `web3`: A web3 client used to interact with the blockchain being crawled.
2. `state_provider`: An [`EthereumStateProvider`][moonworm.crawler.ethereum_state_provider.EthereumStateProvider]
instance that the crawler uses to access blockchain state and event logs.
3. `contract_address`: Checksum address for the smart contract
4. `contract_abi`: List representing objects in the smart contract ABI. It does not need to be an
exhaustive ABI. Any events not present in the ABI will not be crawled. Any methods not present
in the ABI will be signalled as warnings by the crawler but not stored in the crawldata.
5. `num_confirmations`: The crawler will remain this many blocks behind the current head of the blockchain.
6. `sleep_time`: The number of seconds for which to wait between polls of the state provider. Useful
if the provider rate limits clients.
7. `start_block`: Optional block number from which to start the crawl. If not provided, crawl will
start at block 0.
8. `end_block`: Optional block number at which to end crawl. If not provided, crawl will continue
indefinitely.
9. `min_blocks_batch`: Minimum number of blocks to process at a time. The crawler adapts the batch
size based on the volume of events and transactions it parses for the contract in its current
range of blocks.
10. `min_blocks_batch`: Minimum number of blocks to process at a time. The crawler adapts the batch
size based on the volume of events and transactions it parses for the contract in its current
range of blocks.
11. `max_blocks_batch`: Maximum number of blocks to process at a time. The crawler adapts the batch
size based on the volume of events and transactions it parses for the contract in its current
range of blocks.
12. `batch_size_update_threshold`: Adaptive parameter used to update batch size of blocks crawled
based on number of events processed in the current batch.
13. `only_events`: If this argument is set to True, the crawler will only crawl events and ignore
method calls. Crawling events is much, much faster than crawling method calls.
14. `outfile`: An optional file to which to write events and/or method calls in [JSON Lines format](https://jsonlines.org/).
Data is written to this file in append mode, so the crawler never deletes old data.
## Outputs
None. Results are printed to stdout and, if an outfile has been provided, also to the file.
"""
def _crawl_events(