from datetime import datetime from functools import lru_cache from pendulum.parsing.exceptions import ParserError import pendulum import os from pytezos.rpc.context import Context from pytezos.rpc.node import RpcQuery, urljoin from pytezos.rpc.operation import Operation, OperationListList from pytezos.rpc.votes import Votes from pytezos.rpc.helpers import HelpersMixin from pytezos.crypto import blake2b_32, Key from pytezos.encoding import base58_encode, is_bh def to_timestamp(v): try: v = pendulum.parse(v) except ParserError: pass if isinstance(v, datetime): v = int(v.timestamp()) return v class BlockListList(RpcQuery): def __call__(self, length=1, head=None, min_date=None): if isinstance(head, str) and not is_bh(head): head = self.__getitem__(head).calculate_hash() if min_date and not isinstance(min_date, int): min_date = to_timestamp(min_date) return super(BlockListList, self).__call__(length=length, head=head, min_date=min_date) def __getitem__(self, item): if isinstance(item, slice): if not isinstance(item.start, int): raise NotImplementedError('Slice start should be an integer.') if item.stop is None: block_id = 'head' elif isinstance(item.stop, int): if item.stop < 0: block_id = f'head~{abs(item.stop)}' else: block_id = item.stop else: raise NotImplementedError('Slice stop can be an integer or None.') header = self.__getitem__(block_id).header() if item.start < 0: length = abs(item.start) if isinstance(item.stop, int) and item.stop < 0: length -= abs(item.stop) else: length = header['level'] - item.start return self.__call__(length=length, head=header['hash']) return super(BlockListList, self).__getitem__(item) class BlockHeader(RpcQuery, HelpersMixin): def __init__(self, *args, **kwargs): super(BlockHeader, self).__init__( properties=['shell', 'protocol_data', 'raw'], *args, **kwargs) def watermark(self): return '01' + self.get_chain_watermark() def unsigned_data(self): data = self.shell() data['protocol_data'] = self.protocol_data.signed_bytes()[:-128] return data def unsigned_bytes(self): return self.watermark() + self.forge() def calculate_hash(self): hash_digest = blake2b_32(self.raw()).digest() return base58_encode(hash_digest, b'B').decode() def calculate_pow_stamp(self): hash_digest = blake2b_32(self.forge() + '0' * 128).digest() return int.from_bytes(hash_digest, byteorder='big') def forge(self): data = self._node.post( path='chains/main/blocks/head/helpers/forge_block_header', json=self.unsigned_data() ) return data['block'] class Block(RpcQuery, HelpersMixin): def __init__(self, *args, **kwargs): kwargs.update( cache='head' not in kwargs.get('path', ''), block_id=os.path.basename(kwargs.get('path', '')) ) super(Block, self).__init__( properties={ 'hash': RpcQuery, 'header': BlockHeader, 'context': Context, 'metadata': RpcQuery }, *args, **kwargs) @property @lru_cache(maxsize=None) def operations(self): return OperationListList( path=f'{self._path}/operations', node=self._node, child_class=Operation, **self._kwargs ) @property @lru_cache(maxsize=None) def operation_hashes(self): return OperationListList( path=f'{self._path}/operation_hashes', node=self._node, child_class=RpcQuery, **self._kwargs ) @property def votes(self): return Votes( path=f'{self._path}/votes', node=self._node, **self._kwargs ) def freeze(self): """ Returns fixed-hash block, useful for aliases, like head, head~1, etc. :return: Block instance with hash initialized """ return Block( path=urljoin(os.path.dirname(self._path), self.hash()), node=self._node, **self._kwargs ) @property def predecessor(self): return Block( path=urljoin(os.path.dirname(self._path), self.header.get('predecessor')), node=self._node, **self._kwargs ) def level(self) -> int: return self.metadata.get('level')['level'] def cycle(self) -> int: return self.metadata.get('level')['cycle'] def create_endorsement(self) -> Operation: header = self.header() return Operation(data={ 'branch': header['hash'], 'contents': [{ 'kind': 'endorsement', 'level': header['level'] }] }) def verify_signature(self): pk = self.get_public_key(self.metadata.get('baker')) Key(pk).verify(self.header.get('signature'), self.header.unsigned_bytes())