Add BlockCall model. Use it in Block

This commit is contained in:
Luke Van Seters 2021-07-12 15:12:23 -04:00
parent 14fc2396f3
commit 6020e48c31
5 changed files with 132 additions and 78 deletions

View File

@ -4,54 +4,39 @@ from typing import List
from web3 import Web3 from web3 import Web3
from schemas import Block from schemas import Block, BlockCall, BlockCallType
cache_directory = './cache' cache_directory = './cache'
def get_transaction_hashes(calls: List[dict]) -> List[str]:
result = []
for call in calls:
if call['type'] != 'reward':
if call['transactionHash'] in result:
continue
else:
result.append(call['transactionHash'])
return result
def write_json(block: Block):
cache_path = _get_cache_path(block.block_number)
write_mode = "w" if cache_path.is_file() else "x"
with open(cache_path, mode=write_mode) as cache_file:
cache_file.write(block.json(sort_keys=True, indent=4))
## Creates a block object, either from the cache or from the chain itself ## Creates a block object, either from the cache or from the chain itself
## Note that you need to pass in the provider, not the web3 wrapped provider object! ## Note that you need to pass in the provider, not the web3 wrapped provider object!
## This is because only the provider allows you to make json rpc requests ## This is because only the provider allows you to make json rpc requests
def createFromBlockNumber(block_number: int, base_provider) -> Block: def createFromBlockNumber(block_number: int, base_provider) -> Block:
cache_path = _get_cache_path(block_number) cache_path = _get_cache_path(block_number)
## Check to see if the data already exists in the cache
## if it exists load the data from cache
## If not then get the data from the chain and save it to the cache
if (cache_path.is_file()): if (cache_path.is_file()):
print( print(
f'Cache for block {block_number} exists, ' \ f'Cache for block {block_number} exists, ' \
'loading data from cache' 'loading data from cache'
) )
block = Block.parse_file(cache_path) return Block.parse_file(cache_path)
return block
else: else:
w3 = Web3(base_provider) print(
print(("Cache for block {block_number} did not exist, getting data").format(block_number=block_number)) f"Cache for block {block_number} did not exist, getting data"
)
w3 = Web3(base_provider)
block = fetch_block(w3, base_provider, block_number)
cache_block(cache_path, block)
return block
def fetch_block(w3, base_provider, block_number: int) -> Block:
## Get block data ## Get block data
block_data = w3.eth.get_block(block_number, True) block_data = w3.eth.get_block(block_number, True)
@ -61,7 +46,11 @@ def createFromBlockNumber(block_number: int, base_provider) -> Block:
block_receipts_raw = base_provider.make_request("eth_getBlockReceipts", [block_number]) block_receipts_raw = base_provider.make_request("eth_getBlockReceipts", [block_number])
## Trace the whole block, return those calls ## Trace the whole block, return those calls
block_calls = w3.parity.trace_block(block_number) block_calls_json = w3.parity.trace_block(block_number)
block_calls = [
BlockCall(**call_json)
for call_json in block_calls_json
]
## Get the logs ## Get the logs
block_hash = (block_data.hash).hex() block_hash = (block_data.hash).hex()
@ -69,6 +58,7 @@ def createFromBlockNumber(block_number: int, base_provider) -> Block:
## Get gas used by individual txs and store them too ## Get gas used by individual txs and store them too
txs_gas_data = {} txs_gas_data = {}
for transaction in block_data['transactions']: for transaction in block_data['transactions']:
tx_hash = (transaction.hash).hex() tx_hash = (transaction.hash).hex()
tx_data = w3.eth.get_transaction(tx_hash) tx_data = w3.eth.get_transaction(tx_hash)
@ -82,7 +72,7 @@ def createFromBlockNumber(block_number: int, base_provider) -> Block:
transaction_hashes = get_transaction_hashes(block_calls) transaction_hashes = get_transaction_hashes(block_calls)
## Create a new object ## Create a new object
block = Block( return Block(
block_number=block_number, block_number=block_number,
data=block_data, data=block_data,
receipts=block_receipts_raw, receipts=block_receipts_raw,
@ -92,10 +82,23 @@ def createFromBlockNumber(block_number: int, base_provider) -> Block:
txs_gas_data=txs_gas_data, txs_gas_data=txs_gas_data,
) )
## Write the result to a JSON file for loading in the future
write_json(block)
return block def get_transaction_hashes(calls: List[BlockCall]) -> List[str]:
result = []
for call in calls:
if call.type != BlockCallType.reward:
if call.transaction_hash not in result:
result.append(call.transaction_hash)
return result
def cache_block(cache_path: Path, block: Block):
write_mode = "w" if cache_path.is_file() else "x"
with open(cache_path, mode=write_mode) as cache_file:
cache_file.write(block.json())
def _get_cache_path(block_number: int) -> Path: def _get_cache_path(block_number: int) -> Path:

View File

@ -1,3 +1,5 @@
from schemas.utils import to_original_json_dict
class Processor: class Processor:
def __init__(self, base_provider, inspectors) -> None: def __init__(self, base_provider, inspectors) -> None:
@ -7,7 +9,10 @@ class Processor:
def get_transaction_evaluations(self, block_data): def get_transaction_evaluations(self, block_data):
for transaction_hash in block_data.transaction_hashes: for transaction_hash in block_data.transaction_hashes:
calls = block_data.get_filtered_calls(transaction_hash) calls = block_data.get_filtered_calls(transaction_hash)
calls_json = [
to_original_json_dict(call)
for call in calls
]
for inspector in self.inspectors: for inspector in self.inspectors:
inspector.inspect(calls) inspector.inspect(calls_json)
# print(calls)

View File

@ -1 +1 @@
from .blocks import Block from .blocks import Block, BlockCall, BlockCallType

View File

@ -1,14 +1,38 @@
import json import json
from enum import Enum
from typing import Dict, List, Optional from typing import Dict, List, Optional
from hexbytes import HexBytes from hexbytes import HexBytes
from pydantic import BaseModel from pydantic import BaseModel
from web3.datastructures import AttributeDict from web3.datastructures import AttributeDict
from .utils import CamelModel
class BlockCallType(Enum):
call = "call"
create = "create"
delegate_call = "delegateCall"
reward = "reward"
suicide = "suicide"
class BlockCall(CamelModel):
action: dict
block_hash: str
block_number: int
result: Optional[dict]
subtraces: int
trace_address: List[int]
transaction_hash: Optional[str]
transaction_position: Optional[int]
type: BlockCallType
error: Optional[str]
class Block(BaseModel): class Block(BaseModel):
block_number: int block_number: int
calls: List[dict] calls: List[BlockCall]
data: dict data: dict
logs: List[dict] logs: List[dict]
receipts: dict receipts: dict
@ -21,8 +45,8 @@ class Block(BaseModel):
HexBytes: lambda h: h.hex(), HexBytes: lambda h: h.hex(),
} }
def get_filtered_calls(self, hash: str) -> List[dict]: def get_filtered_calls(self, hash: str) -> List[BlockCall]:
return [ return [
call for call in self.calls call for call in self.calls
if call["transactionHash"] == hash if call.transaction_hash == hash
] ]

22
schemas/utils.py Normal file
View File

@ -0,0 +1,22 @@
import json
from pydantic import BaseModel
def to_camel(string: str) -> str:
return ''.join(
word.capitalize() if i > 0 else word
for i, word in enumerate(string.split('_'))
)
class CamelModel(BaseModel):
"""BaseModel that translates from camelCase to snake_case"""
class Config:
alias_generator = to_camel
allow_population_by_field_name = True
def to_original_json_dict(model: BaseModel) -> dict:
return json.loads(model.json(by_alias=True, exclude_unset=True))