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

147
block.py
View File

@ -4,100 +4,103 @@ 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"
## Get block data
block_data = w3.eth.get_block(block_number, True)
## Get the block receipts
## TODO: evaluate whether or not this is sufficient or if gas used needs to be converted to a proper big number.
## In inspect-ts it needed to be converted
block_receipts_raw = base_provider.make_request("eth_getBlockReceipts", [block_number])
## Trace the whole block, return those calls
block_calls = w3.parity.trace_block(block_number)
## Get the logs
block_hash = (block_data.hash).hex()
block_logs = w3.eth.get_logs({'blockHash': block_hash})
## Get gas used by individual txs and store them too
txs_gas_data = {}
for transaction in block_data['transactions']:
tx_hash = (transaction.hash).hex()
tx_data = w3.eth.get_transaction(tx_hash)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
txs_gas_data[tx_hash] = {
'gasUsed': tx_receipt['gasUsed'], # fix: why does this return 0 for certain txs?
'gasPrice': tx_data['gasPrice'],
'netFeePaid': tx_data['gasPrice'] * tx_receipt['gasUsed']
}
transaction_hashes = get_transaction_hashes(block_calls)
## Create a new object
block = Block(
block_number=block_number,
data=block_data,
receipts=block_receipts_raw,
calls=block_calls,
logs=block_logs,
transaction_hashes=transaction_hashes,
txs_gas_data=txs_gas_data,
) )
## Write the result to a JSON file for loading in the future w3 = Web3(base_provider)
write_json(block) block = fetch_block(w3, base_provider, block_number)
cache_block(cache_path, block)
return block return block
def fetch_block(w3, base_provider, block_number: int) -> Block:
## Get block data
block_data = w3.eth.get_block(block_number, True)
## Get the block receipts
## TODO: evaluate whether or not this is sufficient or if gas used needs to be converted to a proper big number.
## In inspect-ts it needed to be converted
block_receipts_raw = base_provider.make_request("eth_getBlockReceipts", [block_number])
## Trace the whole block, return those calls
block_calls_json = w3.parity.trace_block(block_number)
block_calls = [
BlockCall(**call_json)
for call_json in block_calls_json
]
## Get the logs
block_hash = (block_data.hash).hex()
block_logs = w3.eth.get_logs({'blockHash': block_hash})
## Get gas used by individual txs and store them too
txs_gas_data = {}
for transaction in block_data['transactions']:
tx_hash = (transaction.hash).hex()
tx_data = w3.eth.get_transaction(tx_hash)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
txs_gas_data[tx_hash] = {
'gasUsed': tx_receipt['gasUsed'], # fix: why does this return 0 for certain txs?
'gasPrice': tx_data['gasPrice'],
'netFeePaid': tx_data['gasPrice'] * tx_receipt['gasUsed']
}
transaction_hashes = get_transaction_hashes(block_calls)
## Create a new object
return Block(
block_number=block_number,
data=block_data,
receipts=block_receipts_raw,
calls=block_calls,
logs=block_logs,
transaction_hashes=transaction_hashes,
txs_gas_data=txs_gas_data,
)
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:
cache_directory_path = Path(cache_directory) cache_directory_path = Path(cache_directory)
return cache_directory_path / f"{block_number}-new.json" return cache_directory_path / f"{block_number}-new.json"

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))